# Helpers to increase the font size from existing font data.
# Thanks to it we can save space by not having to include
# the larger font definitions.

from __future__ import annotations

from typing_extensions import Literal
from typing import Tuple


Bit = Literal[0, 1]
Point = Tuple[int, int]


def magnify_glyph_by_two(width: int, height: int, bytes_data: list[int]) -> list[int]:
    """Magnifying the font size by two.

    Input and output are bytes (that is the standard in font files),
    but internally works with bits.
    """
    bits_data = _bytes_to_bits(bytes_data, height * width)
    double_size_data = _double_the_bits(width, bits_data)
    return _bits_to_bytes(double_size_data)


def _bytes_to_bits(bytes_data: list[int], bits_to_take: int) -> list[Bit]:
    """Transform bytes into bits."""
    bits_data = [f"{i:08b}" for i in bytes_data[:-1]]

    # Last element needs to be handled carefully,
    # to respect the number of bits to take.
    missing_bits = bits_to_take - len(bits_data) * 8
    last_byte = bytes_data[-1]
    last_binary = f"{last_byte:08b}"
    # Taking either the right or left part of the last byte
    if last_byte < 2**missing_bits:
        bits_data.append(last_binary[-missing_bits:])
    else:
        bits_data.append(last_binary[:missing_bits])

    return [1 if int(x) else 0 for x in "".join(bits_data)]


def _bits_to_bytes(bits_data: list[Bit]) -> list[int]:
    """Transform bits into bytes."""
    bits_str = "".join([str(bit) for bit in bits_data])
    bytes_str_list = [bits_str[i : i + 8] for i in range(0, len(bits_str), 8)]
    # Last element needs to be right-padded to 8 bits
    while len(bytes_str_list[-1]) != 8:
        bytes_str_list[-1] += "0"
    return [int(byte, 2) for byte in bytes_str_list]


def _double_the_bits(width: int, bits_data: list[Bit]) -> list[Bit]:
    """Double the dimension of a given glyph."""
    # Allocate space for the new data - 2*2 bigger than the original
    # Then fill all the new indexes with appropriate bits
    double_size_data: list[Bit] = [0 for _ in range(4 * len(bits_data))]
    for original_index, bit in enumerate(bits_data):
        for new_index in _corresponding_indexes(original_index, width):
            double_size_data[new_index] = bit
    return double_size_data


def _corresponding_indexes(index: int, width: int) -> list[int]:
    """Find the indexes of the four pixels that correspond to the given one."""
    point = _index_to_point(index, width)
    points = _scale_by_two(point)
    new_width = 2 * width
    return [_point_to_index(new_point, new_width) for new_point in points]


def _scale_by_two(point: Point) -> tuple[Point, Point, Point, Point]:
    """Translate one pixel into four adjacent pixels to visually scale it by two."""
    x, y = point
    return (
        (x * 2, y * 2),
        (x * 2 + 1, y * 2),
        (x * 2, y * 2 + 1),
        (x * 2 + 1, y * 2 + 1),
    )


def _point_to_index(point: Point, width: int) -> int:
    """Translate point to index according to the glyph width."""
    x, y = point
    assert 0 <= x < width
    return y * width + x


def _index_to_point(index: int, width: int) -> Point:
    """Translate index to point according to the glyph width."""
    x = index % width
    y = index // width
    return x, y


def print_from_bits(width: int, height: int, bit_data: list[Bit]):
    """Print the glyph into terminal from bit data."""
    for line_id in range(height):
        line = bit_data[line_id * width : (line_id + 1) * width]
        str_line = "".join([str(x) for x in line])
        print(str_line.replace("0", " "))
    print()


def print_from_bytes(width: int, height: int, bytes_data: list[int]):
    """Print the glyph into terminal from byte data."""
    bits_data = _bytes_to_bits(bytes_data, height * width)
    print_from_bits(width, height, bits_data)


################################
# TEST SECTION for pytest
################################

# fmt: off
HEIGHT = 7
K_WIDTH = 5
K_GLYPH = [140, 169, 138, 74, 32]
K_BITS = [1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1]
K_MAGNIFIED = [192, 240, 60, 51, 12, 204, 51, 15, 3, 192, 204, 51, 12, 51, 12, 192, 240, 48]
M_GLYPH = [131, 7, 29, 89, 48, 96, 128]
M_WIDTH = 7
# fmt: on


def test_bits_to_bytes_and_back():
    vectors = (  # height, width, bytes_data
        (HEIGHT, K_WIDTH, K_GLYPH),
        (HEIGHT, M_WIDTH, M_GLYPH),
    )

    for height, width, bytes_data in vectors:
        bits_data = _bytes_to_bits(bytes_data, height * width)
        assert _bits_to_bytes(bits_data) == bytes_data


def test_bit_to_bytes():
    assert _bytes_to_bits(K_GLYPH, HEIGHT * K_WIDTH) == K_BITS


def test_overall_magnify():
    assert magnify_glyph_by_two(K_WIDTH, HEIGHT, K_GLYPH) == K_MAGNIFIED


if __name__ == "__main__":
    # Print letter K, magnify it and print it also
    print("K_GLYPH", K_GLYPH)
    print_from_bytes(K_WIDTH, HEIGHT, K_GLYPH)
    magnified_data = magnify_glyph_by_two(K_WIDTH, HEIGHT, K_GLYPH)
    print("magnified_data", magnified_data)
    print_from_bytes(2 * K_WIDTH, 2 * HEIGHT, magnified_data)