<?php

declare(strict_types=1);

namespace App\Domain\QrCode\Service;

use App\Domain\QrCode\Exception\QrCodeException;
use App\Domain\QrCode\QrCode;
use App\Domain\QrCode\ValueObject\QrCodeVersion;

class QrCodeEncoder
{
    // Modus-Indikatoren (4 Bit)
    private const MODE_NUMERIC = '0001';
    private const MODE_ALPHANUMERIC = '0010';
    private const MODE_BYTE = '0100';
    private const MODE_KANJI = '1000';

    // Zeichensatz für alphanumerische Kodierung
    private const ALPHANUMERIC_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';

    // Character Count Indicator Längen basierend auf QR-Version und Modus
    private const CHARACTER_COUNT_BITS = [
        // Modus => [Versionsbereich => Bits]
        'numeric' => [
            [1, 9, 10],   // Versionen 1-9: 10 Bits
            [10, 26, 12], // Versionen 10-26: 12 Bits
            [27, 40, 14], // Versionen 27-40: 14 Bits
        ],
        'alphanumeric' => [
            [1, 9, 9],    // Versionen 1-9: 9 Bits
            [10, 26, 11], // Versionen 10-26: 11 Bits
            [27, 40, 13], // Versionen 27-40: 13 Bits
        ],
        'byte' => [
            [1, 9, 8],    // Versionen 1-9: 8 Bits
            [10, 26, 16], // Versionen 10-26: 16 Bits
            [27, 40, 16], // Versionen 27-40: 16 Bits
        ],
        'kanji' => [
            [1, 9, 8],    // Versionen 1-9: 8 Bits
            [10, 26, 10], // Versionen 10-26: 10 Bits
            [27, 40, 12], // Versionen 27-40: 12 Bits
        ],
    ];

    /**
     * Kodiert die Daten in ein QR-Code-Bitstream
     *
     * @param QrCode $qrCode QR-Code mit zu kodierenden Daten
     * @param QrCodeVersion $version Zu verwendende QR-Code-Version
     * @return string Binärer String ('0' und '1')
     * @throws QrCodeException Bei Kodierungsproblemen
     */
    public function encode(QrCode $qrCode, QrCodeVersion $version): string
    {
        $data = $qrCode->getData();

        // 1. Bestimme den optimalen Modus
        $mode = $this->determineEncodingMode($data);

        // 2. Füge Mode Indicator hinzu (4 Bits)
        $encoded = $this->getModeIndicator($mode);

        // 3. Füge Character Count Indicator hinzu
        $encoded .= $this->getCharacterCountIndicator(strlen($data), $mode, $version);

        // 4. Kodiere die eigentlichen Daten
        $encoded .= $this->encodeData($data, $mode);

        // 5. Füge Terminator hinzu und fülle auf Byte-Grenze auf
        $encoded = $this->addTerminator($encoded, $version, $qrCode->getErrorCorrectionLevel());
        $encoded = $this->padToByteBoundary($encoded);

        // 6. Fülle mit Pad-Bytes auf die volle Kapazität
        $encoded = $this->addPadBytes($encoded, $version, $qrCode->getErrorCorrectionLevel());

        return $encoded;
    }

    /**
     * Bestimmt den optimalen Kodierungsmodus für die Daten
     */
    private function determineEncodingMode(string $data): string
    {
        // Prüfe, ob die Daten nur Ziffern enthalten
        if (preg_match('/^[0-9]+$/', $data)) {
            return 'numeric';
        }

        // Prüfe, ob die Daten nur alphanumerische Zeichen enthalten
        if (preg_match('/^[0-9A-Z $%*+\-.\/:]+$/', $data)) {
            return 'alphanumeric';
        }

        // Kanji-Erkennung ist komplex und wird hier nicht implementiert
        // Bei Bedarf könnte hier eine Kanji-Zeichenerkennung erfolgen

        // Standardmodus: Byte-Modus
        return 'byte';
    }

    /**
     * Liefert den Mode Indicator für den angegebenen Modus
     */
    private function getModeIndicator(string $mode): string
    {
        return match ($mode) {
            'numeric' => self::MODE_NUMERIC,
            'alphanumeric' => self::MODE_ALPHANUMERIC,
            'kanji' => self::MODE_KANJI,
            default => self::MODE_BYTE,
        };
    }

    /**
     * Erzeugt den Character Count Indicator
     */
    private function getCharacterCountIndicator(int $length, string $mode, QrCodeVersion $version): string
    {
        $versionNumber = $version->getValue();
        $bits = 0;

        // Bestimme die Anzahl der Bits für den Character Count Indicator
        foreach (self::CHARACTER_COUNT_BITS[$mode] as $range) {
            if ($versionNumber >= $range[0] && $versionNumber <= $range[1]) {
                $bits = $range[2];
                break;
            }
        }

        // Konvertiere die Länge in binäre Darstellung mit der richtigen Anzahl von Bits
        return str_pad(decbin($length), $bits, '0', STR_PAD_LEFT);
    }

    /**
     * Kodiert die Daten im angegebenen Modus
     */
    private function encodeData(string $data, string $mode): string
    {
        return match ($mode) {
            'numeric' => $this->encodeNumeric($data),
            'alphanumeric' => $this->encodeAlphanumeric($data),
            'kanji' => $this->encodeKanji($data),
            default => $this->encodeByte($data),
        };
    }

    /**
     * Kodiert Daten im numerischen Modus
     * Im numerischen Modus werden jeweils 3 Ziffern zu 10 Bits gruppiert
     */
    private function encodeNumeric(string $data): string
    {
        $result = '';
        $length = strlen($data);

        for ($i = 0; $i < $length; $i += 3) {
            $chunk = substr($data, $i, min(3, $length - $i));
            $value = (int)$chunk;

            // Bestimme die Anzahl der Bits basierend auf der Chunk-Länge
            $numBits = match (strlen($chunk)) {
                1 => 4,
                2 => 7,
                3 => 10,
            };

            // Konvertiere den Wert in binäre Darstellung
            $result .= str_pad(decbin($value), $numBits, '0', STR_PAD_LEFT);
        }

        return $result;
    }

    /**
     * Kodiert Daten im alphanumerischen Modus
     * Im alphanumerischen Modus werden jeweils 2 Zeichen zu 11 Bits gruppiert
     */
    private function encodeAlphanumeric(string $data): string
    {
        $result = '';
        $length = strlen($data);

        for ($i = 0; $i < $length; $i += 2) {
            if ($i + 1 < $length) {
                // Zwei Zeichen verarbeiten
                $char1 = strpos(self::ALPHANUMERIC_CHARS, $data[$i]);
                $char2 = strpos(self::ALPHANUMERIC_CHARS, $data[$i + 1]);

                if ($char1 === false || $char2 === false) {
                    throw new QrCodeException("Ungültiges alphanumerisches Zeichen: {$data[$i]} oder {$data[$i+1]}");
                }

                $value = $char1 * 45 + $char2;
                $result .= str_pad(decbin($value), 11, '0', STR_PAD_LEFT);
            } else {
                // Einzelnes letztes Zeichen
                $char = strpos(self::ALPHANUMERIC_CHARS, $data[$i]);

                if ($char === false) {
                    throw new QrCodeException("Ungültiges alphanumerisches Zeichen: {$data[$i]}");
                }

                $result .= str_pad(decbin($char), 6, '0', STR_PAD_LEFT);
            }
        }

        return $result;
    }

    /**
     * Kodiert Daten im Byte-Modus
     * Im Byte-Modus wird jedes Zeichen als 8-Bit-Wert kodiert
     */
    private function encodeByte(string $data): string
    {
        $result = '';
        $length = strlen($data);

        for ($i = 0; $i < $length; $i++) {
            // Konvertiere das Zeichen in seinen ASCII-Wert
            $value = ord($data[$i]);
            $result .= str_pad(decbin($value), 8, '0', STR_PAD_LEFT);
        }

        return $result;
    }

    /**
     * Kodiert Daten im Kanji-Modus
     * Dieser Modus ist für japanische Schriftzeichen optimiert
     * In dieser vereinfachten Implementierung wird er nicht vollständig unterstützt
     */
    private function encodeKanji(string $data): string
    {
        // Vereinfachte Implementierung - in einer realen Anwendung würde hier
        // eine Konvertierung von Kanji-Zeichen in die entsprechenden Werte erfolgen
        return $this->encodeByte($data);
    }

    /**
     * Fügt den Terminator (0000) hinzu und füllt auf bis zur maximalen Kapazität
     */
    private function addTerminator(string $encoded, QrCodeVersion $version, $errorCorrectionLevel): string
    {
        $dataCapacityBits = $version->getDataCapacity($errorCorrectionLevel) * 8;
        $currentLength = strlen($encoded);

        // Berechne, wie viele Terminator-Bits hinzugefügt werden können
        $terminatorLength = max(0, min(4, $dataCapacityBits - $currentLength));

        // Füge den Terminator hinzu
        $encoded .= str_repeat('0', $terminatorLength);

        return $encoded;
    }

    /**
     * Füllt den Bitstream auf eine Byte-Grenze (Multiple von 8 Bits) auf
     */
    private function padToByteBoundary(string $encoded): string
    {
        $remainder = strlen($encoded) % 8;

        if ($remainder > 0) {
            $encoded .= str_repeat('0', 8 - $remainder);
        }

        return $encoded;
    }

    /**
     * Füllt den Bitstream mit Pad-Bytes auf bis zur maximalen Kapazität
     */
    private function addPadBytes(string $encoded, QrCodeVersion $version, $errorCorrectionLevel): string
    {
        $dataCapacityBits = $version->getDataCapacity($errorCorrectionLevel) * 8;
        $currentLength = strlen($encoded);

        // Wenn die Daten bereits die Kapazität erreicht haben, nichts tun
        if ($currentLength >= $dataCapacityBits) {
            return $encoded;
        }

        // Pad-Bytes-Muster (wechselt zwischen 11101100 und 00010001)
        $padBytes = ['11101100', '00010001'];
        $padIndex = 0;

        // Füge Pad-Bytes hinzu, bis die Kapazität erreicht ist
        while ($currentLength < $dataCapacityBits) {
            $encoded .= $padBytes[$padIndex];
            $padIndex = ($padIndex + 1) % 2;
            $currentLength += 8;
        }

        return $encoded;
    }
}
