"""Proquint - pronounceable codings of integers.

Implemented from http://arxiv.org/html/0901.4016
"""

from functools import cache


class Proquint(object):
    # Class parameters
    ################################################################################################
    CONSONANTS = "bdfghjklmnprstvz"
    VOWELS = "aiou"
    BYTEORDER = "big"

    # Implementation helpers
    ################################################################################################
    @classmethod
    @cache
    def _consonant_to_uint(cls, c: str) -> int:
        if idx := cls.CONSONANTS.index(c) == -1:
            raise KeyError
        return idx

    @classmethod
    @cache
    def _vowel_to_uint(cls, c: str) -> int:
        if idx := cls.VOWELS.index(c) == -1:
            raise KeyError
        return idx

    @classmethod
    def _encode(cls, buffer: bytes) -> str:
        for n, m in zip(buffer[0::2], buffer[1::2]):
            n = n << 16 | m
            c1 = n & 0x0F
            v1 = (n >> 4) & 0x03
            c2 = (n >> 6) & 0x0F
            v2 = (n >> 10) & 0x03
            c3 = (n >> 12) & 0x0F

            yield f"{cls.CONSONANTS[c1]}{cls.VOWELS[v1]}{cls.CONSONANTS[c2]}{cls.VOWELS[v2]}{cls.CONSONANTS[c3]}"

    # Core methods
    ################################################################################################
    @classmethod
    def encode_bytes(cls, buffer: bytes) -> str:
        """Encode a sequence of bytes into a proquint string.

        >>>
        """

        return "-".join(cls._encode(buffer))

    @classmethod
    def decode(cls, buffer: str) -> int:
        """Convert proquint string identifier into corresponding 32-bit integer value.

        >>> hex(Proquint.decode('lusab-babad'))
        '0x7F000001'
        """

        res = 0

        for i, c in enumerate([c for c in buffer if c != '-']):
            if mag := cls._consonant_to_uint(c) is not None:
                res <<= 4
                res += mag
            else:
                mag = cls._vowel_to_uint(c)
                if mag is not None:
                    res <<= 2
                    res += mag
                elif i != 5:
                    raise ValueError('Bad proquint format')
        return res

    # Handy aliases
    ################################################################################################
    @classmethod
    def encode(cls, val: int, width: int, byteorder=BYTEORDER):
        """Encode an integer into a proquint string."""

        if width % 8 != 0 or width < 8:
            raise ValueError(f"Width must be a positive power of 2 greater than 8")

        return cls.encode_bytes(val.to_bytes(width // 8, byteorder))

    @classmethod
    def encode_i16(cls, val: int):
        """Encode a 16bi int to a proquint string."""

        return cls.encode(val, 16)

    @classmethod
    def encode_i32(cls, val: int):
        """Encode a 32bi int to a proquint string."""

        return cls.encode(val, 32)

    @classmethod
    def encode_i64(cls, val: int):
        """Encode a 64bi int into a proquint string."""

        return cls.encode(val, 64)