"""Convert binary data into a human friendly format."""

# fmt: off
WORDLIST  = [
    "acid", "also", "anti", "arch", "area", "atom", "aunt", "baby", "back",
    "ball", "bang", "bare", "barn", "beef", "beep", "beer", "best", "beta",
    "blob", "blow", "boom", "boss", "bush", "call", "calm", "card", "cars",
    "cash", "clay", "coma", "cook", "core", "crab", "crop", "damp", "dark",
    "dash", "data", "dawn", "deaf", "deal", "deer", "deny", "dice", "disc",
    "dogs", "draw", "drug", "drum", "dust", "east", "easy", "eggs", "else",
    "epic", "etch", "ever", "evil", "exam", "face", "fact", "fawn", "film",
    "fish", "flag", "flaw", "flea", "flux", "food", "four", "full", "funk",
    "fury", "fuzz", "gain", "game", "gang", "gasp", "gear", "germ", "gift",
    "girl", "glow", "gold", "grab", "guts", "hair", "half", "hand", "harm",
    "hazy", "help", "herb", "hero", "high", "hill", "hiss", "horn", "hurt",
    "husk", "hype", "icon", "idea", "idle", "indy", "info", "iris", "itch",
    "item", "jade", "jail", "jaws", "join", "jump", "jury", "just", "kale",
    "keen", "keto", "kick", "king", "kiss", "kiwi", "knob", "lady", "lake",
    "lamp", "last", "leaf", "lens", "liar", "lion", "logo", "long", "lord",
    "luck", "lush", "mage", "mail", "many", "mars", "math", "memo", "menu",
    "meta", "mild", "mini", "moon", "must", "nail", "name", "navy", "neck",
    "need", "next", "noon", "norm", "nuts", "oath", "once", "orca", "oval",
    "over", "page", "paid", "palm", "path", "pawn", "ping", "pins", "play",
    "pool", "poor", "port", "puff", "pump", "quit", "race", "raid", "rain",
    "ramp", "rash", "rats", "rear", "redo", "reef", "ring", "risk", "room",
    "ruby", "rust", "safe", "sail", "salt", "sand", "scar", "ship", "sick",
    "sign", "sing", "slab", "slow", "soda", "solo", "stay", "surf", "swim",
    "taco", "talk", "taxi", "team", "tech", "text", "tiny", "tips", "toad",
    "tofu", "tomb", "tool", "tour", "trap", "tuna", "turf", "twig", "twin",
    "type", "ugly", "undo", "urge", "user", "very", "veto", "vial", "visa",
    "void", "volt", "vote", "walk", "wall", "warn", "warp", "wash", "wear",
    "west", "wind", "wing", "wire", "wolf", "worm", "yank", "yard", "yeah",
    "yell", "yoga", "zeus", "zone",
]
"""Pricklybird wordlist mapping each byte value to a word."""
# fmt: on


def word_hash(word_first_letter: str, word_last_letter: str) -> int:
    """Perfect hash function to index the `HASH_TABLE`.

    Utilizes the fact that all words in the wordlist have unique first,
     last letter pairs.

    # Usage
    >>> from pypricklybird import word_hash, HASH_TABLE
    >>> word = "flux"
    >>> table_index = word_hash(word[0], word[-1])
    >>> table_index
    603
    >>> hex(HASH_TABLE[table_index])
    '0x43'
    """
    first_letter_index = ord(word_first_letter[0])
    last_letter_index = ord(word_last_letter[0])
    if not (ord("a") <= first_letter_index <= ord("z")):
        return 0
    if not (ord("a") <= last_letter_index <= ord("z")):
        return 0
    return (first_letter_index - ord("a")) + (last_letter_index - ord("a")) * 26


# The highest hash value generated by `word_hash` is 656 for words in the wordlist,
# but with incorrect input where the first and last letter are `z`,
# the value might reach 26 ** 2 - 1 = 675.
# So we make the table 676 entries long to prevent out of bounds errors.
HASH_TABLE_SIZE = 676


def _generate_hashtable(wordlist: list[str]) -> list[int]:
    """Prepare word to byte hashtable.

    Generate hash table by calculating hash for every word and populating
    the hash table position with that words byte value.
    """
    result = [0] * HASH_TABLE_SIZE
    for i, word in enumerate(wordlist):
        result[word_hash(word[0], word[-1])] = i
    return result


HASH_TABLE = _generate_hashtable(WORDLIST)
"""Hash table used to map words to byte values.

Indexed by giving the `word_hash` function the words first and last letter.
Contains the matching byte value.
"""

CRC8_POLY = 0x1D
"""Polynominal used in CRC-8 calculation."""

_N_BYTES = 256
"""Number of different possible values in a byte."""


def _generate_crc_table(polynominal: int) -> list[int]:
    """Precomputes `CRC8_TABLE` to speed up CRC-8 calculations."""
    table = [0] * _N_BYTES
    for i in range(_N_BYTES):
        crc = i
        for _ in range(8):
            if crc & 0x80:
                crc = (crc << 1) ^ polynominal
            else:
                crc <<= 1
            crc &= 0xFF
        table[i] = crc
    return table


# Table of CRC8 values for all possible bytes, allows fast calculation of CRC.
CRC8_TABLE = _generate_crc_table(CRC8_POLY)


class DecodeError(Exception):
    """Unable to decode pricklybird words."""


class CRCError(DecodeError):
    """Invalid CRC detected while decoding."""


def calculate_crc8(data: bytes | bytearray) -> bytes:
    """Calculate the CRC-8 used by pricklybird based on a precomputed table.

    # CRC parameters
    - Output width of 8 bits
    - Division using the polynomial `0x1D`
    - An initial value of zero
    - No input or output reflection
    - No XOR operation on the output
    - Remainder after division of data with correct CRC appended is zero

    # Usage
    >>> from pypricklybird import calculate_crc8
    >>> data = bytearray()
    >>> data.extend("123456789".encode())
    >>> calculate_crc8(data).hex()
    '37'
    """
    crc = 0

    for byte in data:
        crc = CRC8_TABLE[(crc ^ byte)]
    return crc.to_bytes(1)


def bytes_to_words(data: bytes | bytearray) -> list[str]:
    """Convert bytearray to list of pricklybird words.

    Return a list of words with each input byte mapped to the matching pricklybird word.

    # Usage
    >>> from pypricklybird import bytes_to_words
    >>> data = bytearray.fromhex("4243")
    >>> bytes_to_words(data)
    ['flea', 'flux']
    """
    return [WORDLIST[int(b)] for b in data]


def convert_to_pricklybird(data: bytes | bytearray) -> str:
    """Convert arbitrary data to pricklybird words and attach CRC.

    # Usage
    >>> from pypricklybird import convert_to_pricklybird
    >>> data = bytearray.fromhex("4243")
    >>> convert_to_pricklybird(data)
    'flea-flux-full'
    """
    if not data:
        return ""
    return "-".join(bytes_to_words(data + calculate_crc8(data)))


def words_to_bytes(words: list[str]) -> bytearray:
    """Map a list of pricklybird words to their coresponding bytes.

    # Errors
    Will raise `DecodeError` if:
    - The input contains non ASCII compatible characters
    - The words in the input are not all four characters long
    - Words in the input dont appear in the wordlist

    # Usage
    >>> from pypricklybird import words_to_bytes
    >>> words_to_bytes(["flea", "flux"]).hex()
    '4243'
    """
    data = bytearray(len(words))
    for i, word in enumerate(words):
        if not word.isascii():
            raise DecodeError("Input must only contain ASCII compatible UTF-8.")
        if len(word) != 4:
            raise DecodeError("Input words must be four characters long.")

        word_lower = word.lower()
        index = HASH_TABLE[word_hash(word_lower[0], word_lower[-1])]
        if WORDLIST[index] != word_lower:
            raise DecodeError("Invalid word detected in input.")
        data[i] = index
    return data


def convert_from_pricklybird(words: str) -> bytearray:
    """Convert pricklybird words to bytearray and check CRC.

    # Errors
    Will raise `DecodeError` if:
    - The input is less than two words long,
    - The input contains non ASCII compatible characters
    - The words in the input are not all four characters long
    - Words in the input dont appear in the wordlist

    Will raise `CRCError` if the CRC value does not match the input.

    # Usage
    >>> from pypricklybird import convert_from_pricklybird
    >>> convert_from_pricklybird("flea-flux-full").hex()
    '4243'
    """
    word_list = words.strip().split("-")
    if len(word_list) < 2:  # Need at least data + CRC
        raise DecodeError("Input must be at least two words long.")

    data = words_to_bytes(word_list)
    if calculate_crc8(data) != b"\x00":
        raise CRCError("Invalid CRC while decoding words.")
    return data[:-1]
