<?php

declare(strict_types=1);

namespace App\Framework\Core\Encoding;

use InvalidArgumentException;

/**
 * Multi-Standard Base32 Encoder/Decoder
 *
 * Provides Base32 encoding and decoding functionality supporting multiple standards:
 * - RFC 3548 (standard): Used for TOTP secrets and general applications
 * - Crockford's Base32: Used for ULIDs and human-readable identifiers
 */
final readonly class Base32Encoder
{
    // Padding character
    private const string PADDING = '=';

    /**
     * Encode binary data to Base32 string using RFC 3548 alphabet
     */
    public static function encode(string $data): string
    {
        return self::encodeWithAlphabet($data, Base32Alphabet::RFC3548);
    }

    /**
     * Encode binary data using Crockford's Base32 alphabet (for ULID)
     */
    public static function encodeCrockford(string $data): string
    {
        return self::encodeWithAlphabet($data, Base32Alphabet::CROCKFORD);
    }

    /**
     * Encode binary data to Base32 string with specified alphabet
     */
    public static function encodeWithAlphabet(string $data, Base32Alphabet $alphabet): string
    {
        if (empty($data)) {
            return '';
        }

        $encoded = '';
        $dataLength = strlen($data);

        // Process data in 5-byte chunks (40 bits)
        for ($i = 0; $i < $dataLength; $i += 5) {
            $chunk = substr($data, $i, 5);
            $chunkLength = strlen($chunk);

            // Pad chunk to 5 bytes if necessary
            $chunk = str_pad($chunk, 5, "\0");

            // Convert 5 bytes (40 bits) to 8 Base32 characters
            $bits = '';
            for ($j = 0; $j < 5; $j++) {
                $bits .= str_pad(decbin(ord($chunk[$j])), 8, '0', STR_PAD_LEFT);
            }

            // Split into 5-bit groups and encode
            for ($j = 0; $j < 8; $j++) {
                $fiveBits = substr($bits, $j * 5, 5);
                if (strlen($fiveBits) < 5) {
                    break;
                }

                $index = bindec($fiveBits);
                $encoded .= $alphabet->getCharacterAt($index);
            }

            // Handle incomplete chunks
            if ($chunkLength < 5) {
                if ($alphabet->usesPadding()) {
                    // Add padding for RFC3548
                    $paddingCount = match ($chunkLength) {
                        1 => 6,
                        2 => 4,
                        3 => 3,
                        4 => 1,
                        default => 0
                    };

                    $encoded = substr($encoded, 0, -$paddingCount);
                    $encoded .= str_repeat(self::PADDING, $paddingCount);
                } else {
                    // For Crockford (no padding), truncate to correct length
                    $actualCharCount = match ($chunkLength) {
                        1 => 2,  // 1 byte = 8 bits = 1.6 chars -> 2 chars
                        2 => 4,  // 2 bytes = 16 bits = 3.2 chars -> 4 chars
                        3 => 5,  // 3 bytes = 24 bits = 4.8 chars -> 5 chars
                        4 => 7,  // 4 bytes = 32 bits = 6.4 chars -> 7 chars
                        default => 8
                    };

                    // Remove excess characters for incomplete chunk
                    $encoded = substr($encoded, 0, strlen($encoded) - (8 - $actualCharCount));
                }
            }
        }

        return $encoded;
    }

    /**
     * Decode Base32 string to binary data using RFC 3548 alphabet
     */
    public static function decode(string $encoded): string
    {
        return self::decodeWithAlphabet($encoded, Base32Alphabet::RFC3548);
    }

    /**
     * Decode Crockford's Base32 string to binary data
     */
    public static function decodeCrockford(string $encoded): string
    {
        return self::decodeWithAlphabet($encoded, Base32Alphabet::CROCKFORD);
    }

    /**
     * Decode Base32 string to binary data with specified alphabet
     */
    public static function decodeWithAlphabet(string $encoded, Base32Alphabet $alphabet): string
    {
        if (empty($encoded)) {
            return '';
        }

        // Remove padding if alphabet uses it
        if ($alphabet->usesPadding()) {
            $encoded = rtrim($encoded, self::PADDING);
        }

        $encoded = strtoupper($encoded);
        $encodedLength = strlen($encoded);

        if (! $alphabet->isValidEncoded($encoded)) {
            throw new InvalidArgumentException("Invalid {$alphabet->name} Base32 string");
        }

        $decoded = '';
        $bits = '';

        // Convert each Base32 character to 5 bits
        for ($i = 0; $i < $encodedLength; $i++) {
            $char = $encoded[$i];
            $index = $alphabet->getCharacterIndex($char);
            $bits .= str_pad(decbin($index), 5, '0', STR_PAD_LEFT);
        }

        // Convert bits to bytes
        $bitsLength = strlen($bits);
        for ($i = 0; $i < $bitsLength; $i += 8) {
            $byte = substr($bits, $i, 8);

            // Only process complete bytes
            if (strlen($byte) === 8) {
                $decoded .= chr(bindec($byte));
            }
        }

        return $decoded;
    }

    /**
     * Validate Base32 string format using RFC 3548 alphabet
     */
    public static function isValidBase32(string $encoded): bool
    {
        return Base32Alphabet::RFC3548->isValidEncoded($encoded);
    }

    /**
     * Validate Base32 string format with specified alphabet
     */
    public static function isValidWithAlphabet(string $encoded, Base32Alphabet $alphabet): bool
    {
        return $alphabet->isValidEncoded($encoded);
    }

    /**
     * Generate a random Base32 string using RFC 3548 alphabet
     */
    public static function generateRandom(int $length = 32): string
    {
        return Base32Alphabet::RFC3548->generateRandom($length);
    }

    /**
     * Generate a random Base32 string with specified alphabet
     */
    public static function generateRandomWithAlphabet(Base32Alphabet $alphabet, int $length = 32): string
    {
        return $alphabet->generateRandom($length);
    }

    /**
     * Format Base32 string with spaces for readability
     */
    public static function formatForDisplay(string $encoded, int $groupSize = 4): string
    {
        if ($groupSize < 1) {
            throw new InvalidArgumentException('Group size must be positive');
        }

        return trim(chunk_split($encoded, $groupSize, ' '));
    }

    /**
     * Remove formatting from Base32 string
     */
    public static function removeFormatting(string $formatted): string
    {
        return preg_replace('/\s+/', '', $formatted);
    }

    /**
     * Get available alphabets
     * @return array<int, Base32Alphabet>
     */
    public static function getAvailableAlphabets(): array
    {
        return Base32Alphabet::cases();
    }

    /**
     * Calculate the decoded length for a given encoded length
     */
    public static function getDecodedLength(string $encoded): int
    {
        $encoded = rtrim($encoded, self::PADDING);
        $encodedLength = strlen($encoded);

        // Each Base32 character represents 5 bits
        $totalBits = $encodedLength * 5;

        // Convert to bytes (8 bits each)
        return (int) floor($totalBits / 8);
    }

    /**
     * Calculate the encoded length for a given binary data length
     */
    public static function getEncodedLength(int $dataLength): int
    {
        if ($dataLength < 0) {
            throw new InvalidArgumentException('Data length cannot be negative');
        }

        // Each byte is 8 bits, Base32 uses 5 bits per character
        $totalBits = $dataLength * 8;

        return (int) ceil($totalBits / 5);
    }
}
