
def from_ascii(text: str) -> str:
    """Converts a string to 8 bit ascii codepoints in a bitstring with 8 bit blocks"""
    return ' '.join(format(ord(c), '08b') for c in text)

def to_ascii(bitstr: str) -> str:
    """Decodes a bitstring with n * 8bit length using 8bit ascii and returns the resulting string"""
    bits = ''.join(c for c in bitstr if c in '01')
    return ''.join(chr(int(bits[i:i+8], 2)) for i in range(0, len(bits), 8))


def to_bits(bitstr: str) -> list[bool]:
    """Parse a bit string (spaces ignored) into a list of booleans."""
    return [c == '1' for c in bitstr if c in '01']


def to_bits_blocks(bitstr: str, block_size: int) -> list[list[bool]]:
    """Split a bit string into fixed-size sublists of booleans."""
    bits = to_bits(bitstr)
    return [bits[i:i + block_size] for i in range(0, len(bits), block_size)]


def to_bitstr(bits: list[bool], n: int = 8) -> str:
    """Generate bit string from list of booleans."""
    raw = ''.join('1' if b else '0' for b in bits)
    return ' '.join(raw[i:i + n] for i in range(0, len(raw), n))

def xor(bitstr_a: str, bitstr_b: str, n: int = 8) -> str:
    """XORs two bitstrings and returns the resulting bitstring. The bitstrings should have the same length; if they dont the shorter one is padded with zeros"""
    a = to_bits(bitstr_a)
    b = to_bits(bitstr_b)
    length = max(len(a), len(b))
    a += [False] * (length - len(a))
    b += [False] * (length - len(b))
    return to_bitstr([x ^ y for x, y in zip(a, b)], n)


def error_pattern(bit_error_probability: float, n: int, count: int) -> str:
    """Generates a bit error pattern with count words of length n, where each bit is flipped with bit_error_probability"""
    import random
    bits = [random.random() < bit_error_probability for _ in range(n * count)]
    raw = ''.join('1' if b else '0' for b in bits)
    return ' '.join(raw[i:i+n] for i in range(0, len(raw), n))


def highlight_errors(original: str, received: str) -> str:
    """Creates a highlight string to identify different chars between the two strings (i.e. incorrectly decoded chars.
    """
    length = max(len(original), len(received))
    original  = original.ljust(length)
    received  = received.ljust(length)
    return ''.join('^' if a != b else ' ' for a, b in zip(original, received))


def highlight_error_affected_chars(error_pattern: str, k: int, n: int) -> str:
    """Creates a highlight string to identify chars that were affected by the error pattern. This works for different k and n. A char is assumed to have 8 bits.
    """
    all_bits = [c == '1' for c in error_pattern if c in '01']
    data_bits = []
    for i in range(0, len(all_bits), n):
        data_bits.extend(all_bits[i:i+k])
    return ''.join(
        '^' if any(data_bits[i:i+8]) else ' '
        for i in range(0, len(data_bits), 8)
    )
