<?php

namespace HeroQR\Core;

use Endroid\QrCode\{Builder\Builder, Matrix\Matrix};
use Endroid\QrCode\{Color\ColorInterface, ErrorCorrectionLevel, Exception\ValidationException, RoundBlockSizeMode};
use Endroid\QrCode\Writer\{Result\ResultInterface, WriterInterface};
use HeroQR\{Contracts\QRCodeGeneratorInterface, DataTypes\DataType};
use HeroQR\Managers\{ColorManager, EncodingManager, LabelManager, LogoManager, OutputManager, WriterManager};

/**
 * Handles the generation of QR codes with customizable options.
 *
 * Provides functionality for generating QR codes in various formats
 * (PNG, SVG, PDF and More) with optional customization options such as shapes,
 * markers, and internal patterns. Ensures flexibility and ease of use
 * for both standard and advanced use cases.
 *
 * @package HeroQR/Core
 */
class QRCodeGenerator implements QrCodeGeneratorInterface
{
    private ?ResultInterface $builder = null;
    private LabelManager $labelManager;
    private bool $roundBlockSizeModeExplicitly;

    /**
     * QRCodeGenerator constructor
     * Initializes the necessary services and sets default values for QR code properties
     *
     * @param string $outputFormat The output format (default is 'getDataUri')
     * @param int $size The size of the QR code (default is 800)
     * @param int $margin The margin around the QR code (default is 10)
     * @param LogoManager $logoManager The logo manager instance
     * @param ColorManager $colorManager The color manager instance
     * @param EncodingManager $encodingManager The encoding manager instance
     */
    public function __construct(
        private int                      $size = 800,
        private int                      $margin = 10,
        private string                   $data = 'https://github.com/AmirezaEb/HeroQR',
        private readonly string          $outputFormat = 'getDataUri',
        private readonly LogoManager     $logoManager = new LogoManager(),
        private readonly ColorManager    $colorManager = new ColorManager(),
        private readonly WriterManager   $writerManager = new WriterManager(),
        private readonly OutputManager   $outputManager = new OutputManager(),
        private readonly EncodingManager $encodingManager = new EncodingManager(),
        private RoundBlockSizeMode $roundBlockSizeMode = RoundBlockSizeMode::Margin,
        private ErrorCorrectionLevel $errorCorrectionLevel = ErrorCorrectionLevel::High
    )
    {
        $this->labelManager = new LabelManager($this->colorManager);
    }

    /**
     * Magic method to return the QR code as a string, based on the specified output format
     *
     * @return string The generated QR code as a string
     */
    public function __toString(): string
    {
        return $this->outputFormat === 'getDataUri' ? $this->getDataUri() : $this->getString();
    }

    /**
     * Generates a QR code in the specified format with optional customization.
     *
     * @param string $format The desired format for the QR code ('gif', 'png', 'svg', 'webp', 'eps', 'pdf', 'binary').
     * @param array $customs Optional customization parameters for the QR code. If provided, they must follow this structure:
     * - 'Shape' ('S1' to 'S4'): Defines the overall shape of the QR code.
     * - 'Marker' ('M1' to 'M4'): Specifies the corner marker styles.
     * - 'Cursor' ('C1' to 'C4'): Adjusts the internal patterns.
     * - Example:
     * - generate('png', [
     *  'Shape' => 'S4',
     *  'Marker' => 'M4',
     *  'Cursor' => 'C4'
     *  ]);
     *
     * @return self The current QRCodeGenerator instance for method chaining.
     * @throws \InvalidArgumentException|ValidationException If the specified format is not supported.
     */
    public function generate(string $format = 'png', array $customs = []): self
    {
        $this->builder = (new Builder(
            writer: $this->validateWriter($format, $customs),
            writerOptions: [],
            validateResult: false,
            data: $this->getData(),
            encoding: $this->encodingManager->getEncoding(),
            errorCorrectionLevel: $this->getErrorCorrectionLevel(),
            size: $this->getSize(),
            margin: $this->getMargin(),
            roundBlockSizeMode: $this->getBlockSizeMode(),
            foregroundColor: $this->colorManager->getColor(),
            backgroundColor: $this->colorManager->getBackgroundColor(),
            labelText: $this->labelManager->getLabel(),
            labelFont: $this->labelManager->getLabelFont(),
            labelAlignment: $this->labelManager->getLabelAlign(),
            labelMargin: $this->labelManager->getLabelMargin(),
            labelTextColor: $this->labelManager->getLabelColor(),
            logoPath: $this->logoManager->getLogoPath(),
            logoResizeToWidth: $this->logoManager->getLogoSize(),
            logoResizeToHeight: $this->logoManager->getLogoSize(),
            logoPunchoutBackground: $this->logoManager->getLogoBackground(),
        ))->build();

        return $this;
    }

    /**
     * Returns the QR code's matrix representation
     * The matrix is a grid of black and white cells representing the QR code
     *
     * @return Matrix The matrix representation of the QR code
     * @throws \RuntimeException If the QR code has not been generated yet
     */
    public function getMatrix(): Matrix
    {
        $this->ensureBuilderExists();
        return $this->outputManager->getMatrix($this->builder);
    }

    /**
     * Get the matrix as an array
     *
     * @return array The QR code matrix represented as a 2D array
     * @throws \RuntimeException If no QR code has been generated yet
     */
    public function getMatrixAsArray(): array
    {
        $this->ensureBuilderExists();
        return $this->outputManager->getMatrixAsArray($this->builder);
    }

    /**
     * Returns the QR code as a raw string
     *
     * @return string The raw string representation of the QR code
     * @throws \RuntimeException If the QR code has not been generated yet
     */
    public function getString(): string
    {
        $this->ensureBuilderExists();
        return $this->outputManager->getString($this->builder);
    }

    /**
     * Returns the QR code as a Base64-encoded data URI
     *
     * @return string The data URI representation of the QR code
     * @throws \RuntimeException If the QR code has not been generated yet
     */
    public function getDataUri(): string
    {
        $this->ensureBuilderExists();
        return $this->outputManager->getDataUri($this->builder);
    }

    /**
     * Save the generated QR code to a file
     *
     * @param string $path The path to save the QR code file
     * @return bool True if the file was saved successfully, false otherwise
     * @throws \InvalidArgumentException If the format is unsupported
     * @throws \RuntimeException If no QR code has been generated yet
     */
    public function saveTo(string $path): bool
    {
        $this->ensureBuilderExists();
        return $this->outputManager->saveTo($this->builder, $path);
    }

    /**
     * Set the data to be encoded in the QR code
     *
     * @param string $data The data to encode
     * @param DataType $type DataType auto validation (default = DataType::Text)
     * @return self
     */
    public function setData(string $data, DataType $type = DataType::Text): self
    {
        $class = $type->value;

        if (!$class::validate($data)) {
            throw new \InvalidArgumentException("Invalid data for type: " . $class::getType());
        }

        if (empty(trim($data))) {
            throw new \InvalidArgumentException('Data cannot be empty.');
        }

        $this->data = $this->dataSanitizer($data, $type);
        return $this;
    }

    /**
     * Get the data value
     *
     * @return string|null The data if available, null otherwise
     */
    public function getData(): ?string
    {
        return $this->data;
    }

    /**
     * Set the size of the QR code
     *
     * @param int $size The size of the QR code
     * @return self
     * @throws \InvalidArgumentException If the size is not a positive integer
     */
    public function setSize(int $size): self
    {
        if ($size <= 0) {
            throw new \InvalidArgumentException('Size must be a positive integer.');
        }

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

    /**
     * Get the size value
     *
     * @return int The size value
     */
    public function getSize(): int
    {
        return $this->size;
    }

    /**
     * Set the margin around the QR code
     *
     * @param int $margin The margin size
     * @return self
     * @throws \InvalidArgumentException If the margin is negative
     */
    public function setMargin(int $margin): self
    {
        if ($margin < 0) {
            throw new \InvalidArgumentException('Margin cannot be negative.');
        }

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

    /**
     * Get the margin value
     *
     * @return int The margin value
     */
    public function getMargin(): int
    {
        return $this->margin;
    }

    /**
     * Set the round block size mode
     *
     * The string can be either:
     * - The enum case name (e.g. "None", "Margin", "Enlarge", "Shrink") — case-insensitive.
     *
     * @param string $mode The round block size mode as a string.
     * @return self
     * @throws \InvalidArgumentException If the given mode is invalid.
     */
    public function setBlockSizeMode(string $mode): self
    {
        $mode = strtolower($mode);

        $validModes = array_merge(
            array_map('strtolower', array_column(RoundBlockSizeMode::cases(), 'name')),
            array_map('strtolower', array_column(RoundBlockSizeMode::cases(), 'value'))
        );

        if (!in_array($mode, $validModes, true)) {
            throw new \InvalidArgumentException(
                'Invalid round block size mode. Accepted values: ' . implode(', ', array_unique($validModes)) . '.'
            );
        }

        foreach (RoundBlockSizeMode::cases() as $case) {
            if (strtolower($case->name) === $mode || strtolower($case->value) === $mode) {
                $this->roundBlockSizeMode = $case;
                break;
            }
        }

        return $this;
    }

    /**
     * Get the current round block size mode of the QR code.
     *
     * @return RoundBlockSizeMode The current block size mode enum instance.
     */
    public function getBlockSizeMode(): RoundBlockSizeMode
    {
        return $this->roundBlockSizeMode;
    }

    /**
     * Set the error correction level for the QR code
     *
     * The string can be either:
     * - The enum case name (e.g. "Low", "Medium", "Quartile", "High") — case-insensitive.
     *
     * @param string $level The error correction level as a string.
     * @return self
     * @throws \InvalidArgumentException If the given level is invalid. Accepted values: low, medium, quartile, high.
     */
    public function setErrorCorrectionLevel(string $level): self
    {
        $level = strtolower($level);

        $validLevels = array_merge(
            array_map('strtolower', array_column(ErrorCorrectionLevel::cases(), 'name')),
            array_map('strtolower', array_column(ErrorCorrectionLevel::cases(), 'value'))
        );

        if (!in_array($level, $validLevels, true)) {
            throw new \InvalidArgumentException(
                'Invalid error correction level. Accepted values: ' . implode(', ', array_unique($validLevels)) . '.'
            );
        }

        foreach (ErrorCorrectionLevel::cases() as $case) {
            if (strtolower($case->name) === $level || strtolower($case->value) === $level) {
                $this->errorCorrectionLevel = $case;
                break;
            }
        }

        return $this;
    }

    /**
     * Get the current error correction level of the QR code
     *
     * @return ErrorCorrectionLevel The current error correction level enum instance.
     */
    public function getErrorCorrectionLevel(): ErrorCorrectionLevel
    {
        return $this->errorCorrectionLevel;
    }

    /**
     * Set the color of the QR code foreground
     *
     * @param string $hexColor The hexadecimal color code
     * @return self
     * @throws \InvalidArgumentException If the color format is invalid
     */
    public function setColor(string $hexColor): self
    {
        $this->colorManager->setColor($hexColor);
        return $this;
    }

    /**
     * Get the color from the color manager
     *
     * @return ColorInterface The color object
     */
    public function getColor(): ColorInterface
    {
        return $this->colorManager->getColor();
    }

    /**
     * Set the background color of the QR code
     *
     * @param string $hexColor The hexadecimal color code
     * @return self
     * @throws \InvalidArgumentException If the color format is invalid
     */
    public function setBackgroundColor(string $hexColor): self
    {
        $this->colorManager->setBackgroundColor($hexColor);
        return $this;
    }

    /**
     * Get the background color from the color manager
     *
     * @return ColorInterface The background color object
     */
    public function getBackgroundColor(): ColorInterface
    {
        return $this->colorManager->getBackgroundColor();
    }

    /**
     * Set the logo to be embedded in the QR code
     *
     * @param string $logoPath The path to the logo file
     * @param int $logoSize The size of the logo (default is 80)
     * @return self
     * @throws \InvalidArgumentException If the logo file does not exist
     */
    public function setLogo(string $logoPath, int $logoSize = 80): self
    {
        if (!file_exists($logoPath)) {
            throw new \InvalidArgumentException('Logo File Does Not Exist');
        }

        $this->logoManager->setLogo($logoPath);
        $this->logoManager->setLogoSize($logoSize);
        return $this;
    }

    /**
     * Get the logo path from the logo manager
     *
     * @return string|null The logo path if available, null otherwise
     */
    public function getLogoPath(): ?string
    {
        return $this->logoManager->getLogoPath();
    }

    /**
     * Set the label properties for the QR code
     *
     * @param string $label The text label to be displayed on the QR code
     * @param string $textAlign The text alignment for the label (default is 'center')
     * @param string $textColor The color of the label text in hexadecimal format (default is '#000000')
     * @param int $fontSize The font size of the label text (default is 50)
     * @param array $margin The margin around the label in the format [top, right, bottom, left] (default is [0, 10, 10, 10])
     * @return self Returns the current instance for method chaining
     * @throws \InvalidArgumentException If the label text is empty
     */
    public function setLabel(
        string $label,
        string $textAlign = 'center',
        string $textColor = '#000000',
        int    $fontSize = 50,
        array  $margin = [0, 10, 10, 10]
    ): self
    {
        if (empty($label)) {
            throw new \InvalidArgumentException('Label cannot be empty');
        }

        $this->labelManager->setLabel($label);
        $this->labelManager->setLabelAlign($textAlign);
        $this->labelManager->setLabelColor($textColor);
        $this->labelManager->setLabelSize($fontSize);
        $this->labelManager->setLabelMargin($margin);

        return $this;
    }

    /**
     * Get the label from the label manager
     *
     * @return string|null The label if available, null otherwise
     */
    public function getLabel(): ?string
    {
        return $this->labelManager->getLabel();
    }

    /**
     * Set the encoding type for the QR code
     *
     * @param string $encoding The encoding type ('UTF-16' ,'UTF-8', 'ASCII', 'ISO-8859-1', 'ISO-8859-5', 'ISO-8859-15') and more...
     * @return self Returns the current instance for method chaining
     * @throws \Exception
     */
    public function setEncoding(string $encoding): self
    {
        $this->encodingManager->setEncoding($encoding);

        return $this;
    }

    /**
     * Encodes the data with type-specific rules and sanitizes the input
     * Supports data types like Email, Phone, and Location
     *
     * @param string $data The raw data to encode
     * @param DataType $type The type of data being encoded (Url, WiFi, Location, Text, Email, Phone)
     * @return string Sanitized and properly formatted data string
     */
    private function dataSanitizer(string $data, DataType $type): string
    {
        $data = htmlspecialchars($data);

        return match ($type) {
            DataType::Email => "mailto:{$data}",
            DataType::Phone => "tel:{$data}",
            DataType::Wifi => "$data",
            DataType::Location => "https://www.google.com/maps?q=$data",
            default => $data,
        };
    }

    /**
     * Validates the writer format and returns an appropriate WriterInterface instance
     * Supports both standard and custom writer formats
     *
     * @param string $format The format of the writer ("svg", "png", "pdf",and more...)
     * @param array $customs An optional array of custom values for M Or C Or S
     * @return WriterInterface
     * @throws \InvalidArgumentException|\RuntimeException
     */
    private function validateWriter(string $format, array $customs): WriterInterface
    {
        return $this->writerManager->getWriter($format, $customs);
    }

    /**
     * Helper method to ensure that the builder has been initialized
     *
     * @throws \RuntimeException If the builder has not been initialized
     */
    private function ensureBuilderExists(): void
    {
        if ($this->builder === null) {
            throw new \Error('No QR Code has been generated. Call generate() first.');
        }
    }
}
