#!/usr/bin/env python3
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>

#
# NOTE: to add a new glyph, add an entry to the `box_chars` dict, then update
# the functions `font_for_cell` and `box_glyph_id` in `kitty/fonts.c`.
#

import math
from functools import partial as p
from itertools import repeat
from typing import (
    Callable, Dict, Generator, Iterable, List, MutableSequence, Optional,
    Sequence, Tuple, cast
)

scale = (0.001, 1., 1.5, 2.)
_dpi = 96.0
BufType = MutableSequence[int]


def set_scale(new_scale: Sequence[float]) -> None:
    global scale
    scale = (new_scale[0], new_scale[1], new_scale[2], new_scale[3])


def thickness(level: int = 1, horizontal: bool = True) -> int:
    pts = scale[level]
    return int(math.ceil(pts * (_dpi / 72.0)))


def draw_hline(buf: BufType, width: int, x1: int, x2: int, y: int, level: int) -> None:
    ' Draw a horizontal line between [x1, x2) centered at y with the thickness given by level '
    sz = thickness(level=level, horizontal=False)
    start = y - sz // 2
    for y in range(start, start + sz):
        offset = y * width
        for x in range(x1, x2):
            buf[offset + x] = 255


def draw_vline(buf: BufType, width: int, y1: int, y2: int, x: int, level: int) -> None:
    ' Draw a vertical line between [y1, y2) centered at x with the thickness given by level '
    sz = thickness(level=level, horizontal=True)
    start = x - sz // 2
    for x in range(start, start + sz):
        for y in range(y1, y2):
            buf[x + y * width] = 255


def half_hline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'left', extend_by: int = 0) -> None:
    x1, x2 = (0, extend_by + width // 2) if which == 'left' else (width // 2 - extend_by, width)
    draw_hline(buf, width, x1, x2, height // 2, level)


def half_vline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'top', extend_by: int = 0) -> None:
    y1, y2 = (0, height // 2 + extend_by) if which == 'top' else (height // 2 - extend_by, height)
    draw_vline(buf, width, y1, y2, width // 2, level)


def get_holes(sz: int, hole_sz: int, num: int) -> List[Tuple[int, ...]]:
    if num == 1:
        pts = [sz // 2]
    elif num == 2:
        ssz = (sz - 2 * hole_sz) // 3
        pts = [ssz + hole_sz // 2, 2 * ssz + hole_sz // 2 + hole_sz]
    elif num == 3:
        ssz = (sz - 3 * hole_sz) // 4
        pts = [ssz + hole_sz // 2, 2 * ssz + hole_sz // 2 + hole_sz, 3 * ssz + 2 * hole_sz + hole_sz // 2]
    holes = []
    for c in pts:
        holes.append(tuple(range(c - hole_sz // 2, c - hole_sz // 2 + hole_sz)))
    return holes


hole_factor = 8


def add_hholes(buf: BufType, width: int, height: int, level: int = 1, num: int = 1) -> None:
    line_sz = thickness(level=level, horizontal=True)
    hole_sz = width // hole_factor
    start = height // 2 - line_sz // 2
    holes = get_holes(width, hole_sz, num)
    for y in range(start, start + line_sz):
        offset = y * width
        for hole in holes:
            for x in hole:
                buf[offset + x] = 0


def add_vholes(buf: BufType, width: int, height: int, level: int = 1, num: int = 1) -> None:
    line_sz = thickness(level=level, horizontal=False)
    hole_sz = height // hole_factor
    start = width // 2 - line_sz // 2
    holes = get_holes(height, hole_sz, num)
    for x in range(start, start + line_sz):
        for hole in holes:
            for y in hole:
                buf[x + width * y] = 0


def hline(buf: BufType, width: int, height: int, level: int = 1) -> None:
    half_hline(buf, width, height, level=level)
    half_hline(buf, width, height, level=level, which='right')


def vline(buf: BufType, width: int, height: int, level: int = 1) -> None:
    half_vline(buf, width, height, level=level)
    half_vline(buf, width, height, level=level, which='bottom')


def hholes(buf: BufType, width: int, height: int, level: int = 1, num: int = 1) -> None:
    hline(buf, width, height, level=level)
    add_hholes(buf, width, height, level=level, num=num)


def vholes(buf: BufType, width: int, height: int, level: int = 1, num: int = 1) -> None:
    vline(buf, width, height, level=level)
    add_vholes(buf, width, height, level=level, num=num)


def corner(buf: BufType, width: int, height: int, hlevel: int = 1, vlevel: int = 1, which: Optional[str] = None) -> None:
    wh = 'right' if which is not None and which in '┌└' else 'left'
    half_hline(buf, width, height, level=hlevel, which=wh, extend_by=thickness(vlevel, horizontal=True) // 2)
    wv = 'top' if which is not None and which in '└┘' else 'bottom'
    half_vline(buf, width, height, level=vlevel, which=wv)


def vert_t(buf: BufType, width: int, height: int, a: int = 1, b: int = 1, c: int = 1, which: Optional[str] = None) -> None:
    half_vline(buf, width, height, level=a, which='top')
    half_hline(buf, width, height, level=b, which='left' if which == '┤' else 'right')
    half_vline(buf, width, height, level=c, which='bottom')


def horz_t(buf: BufType, width: int, height: int, a: int = 1, b: int = 1, c: int = 1, which: Optional[str] = None) -> None:
    half_hline(buf, width, height, level=a, which='left')
    half_hline(buf, width, height, level=b, which='right')
    half_vline(buf, width, height, level=c, which='top' if which == '┴' else 'bottom')


def cross(buf: BufType, width: int, height: int, a: int = 1, b: int = 1, c: int = 1, d: int = 1) -> None:
    half_hline(buf, width, height, level=a)
    half_hline(buf, width, height, level=b, which='right')
    half_vline(buf, width, height, level=c)
    half_vline(buf, width, height, level=d, which='bottom')


def fill_region(buf: BufType, width: int, height: int, xlimits: Iterable[Iterable[float]]) -> None:
    for y in range(height):
        offset = y * width
        for x, (upper, lower) in enumerate(xlimits):
            buf[x + offset] = 255 if upper <= y <= lower else 0
    # Anti-alias the boundary, simple y-axis anti-aliasing
    for x, limits in enumerate(xlimits):
        for yf in limits:
            for ypx in range(int(math.floor(yf)), int(math.ceil(yf)) + 1):
                if 0 <= ypx < height:
                    off = ypx * width + x
                    buf[off] = min(255, buf[off] + int((1 - abs(yf - ypx)) * 255))


def line_equation(x1: int, y1: int, x2: int, y2: int) -> Callable[[int], float]:
    m = (y2 - y1) / (x2 - x1)
    c = y1 - m * x1

    def y(x: int) -> float:
        return m * x + c

    return y


def triangle(buf: BufType, width: int, height: int, left: bool = True) -> None:
    ay1, by1, y2 = 0, height - 1, height // 2
    if left:
        x1, x2 = 0, width - 1
    else:
        x1, x2 = width - 1, 0
    uppery = line_equation(x1, ay1, x2, y2)
    lowery = line_equation(x1, by1, x2, y2)
    xlimits = [(uppery(x), lowery(x)) for x in range(width)]
    fill_region(buf, width, height, xlimits)


def corner_triangle(buf: BufType, width: int, height: int, corner: str) -> None:
    if corner == 'top-right' or corner == 'bottom-left':
        diagonal_y = line_equation(0, 0, width - 1, height - 1)
        if corner == 'top-right':
            xlimits = [(0., diagonal_y(x)) for x in range(width)]
        elif corner == 'bottom-left':
            xlimits = [(diagonal_y(x), height - 1.) for x in range(width)]
    else:
        diagonal_y = line_equation(width - 1, 0, 0, height - 1)
        if corner == 'top-left':
            xlimits = [(0., diagonal_y(x)) for x in range(width)]
        elif corner == 'bottom-right':
            xlimits = [(diagonal_y(x), height - 1.) for x in range(width)]
    fill_region(buf, width, height, xlimits)


def antialiased_1px_line(buf: BufType, width: int, height: int, p1: Tuple[int, int], p2: Tuple[int, int]) -> None:
    # Draw an antialiased line using the Wu algorithm
    x1, y1 = p1
    x2, y2 = p2
    dx, dy = x2 - x1, y2 - y1
    off_limit = height * width
    steep = abs(dx) < abs(dy)

    if steep:
        x1, y1, x2, y2, dx, dy = y1, x1, y2, x2, dy, dx

        def p(x: int, y: int) -> Tuple[int, int]:
            return y, x
    else:
        def p(x: int, y: int) -> Tuple[int, int]:
            return x, y

    if x2 < x1:
        x1, x2, y1, y2 = x2, x1, y2, y1

    def fpart(x: float) -> float:
        return x - int(x)

    def rfpart(x: float) -> float:
        return 1 - fpart(x)

    def putpixel(p: Tuple[int, int], alpha: float) -> None:
        x, y = p
        off = int(x + y * width)
        if 0 <= off < off_limit:
            buf[off] = int(min(buf[off] + (alpha * 255), 255))

    def draw_endpoint(pt: Tuple[int, int]) -> int:
        x, y = pt
        xend = round(x)
        yend = y + grad * (xend - x)
        xgap = rfpart(x + 0.5)
        px, py = int(xend), int(yend)
        putpixel(p(px, py), rfpart(yend) * xgap)
        putpixel(p(px, py+1), fpart(yend) * xgap)
        return px

    grad = dy/dx
    intery = y1 + rfpart(x1) * grad

    xstart = draw_endpoint(p(*p1))
    xend = draw_endpoint(p(*p2))

    if xstart > xend:
        xstart, xend = xend, xstart
    xstart += 1

    for x in range(xstart, xend):
        y = int(intery)
        putpixel(p(x, y), rfpart(intery))
        putpixel(p(x, y+1), fpart(intery))
        intery += grad


def antialiased_line(buf: BufType, width: int, height: int, p1: Tuple[int, int], p2: Tuple[int, int], level: int = 1) -> None:
    th = thickness(level)
    if th < 2:
        return antialiased_1px_line(buf, width, height, p1, p2)
    (x1, y1), (x2, y2) = p1, p2
    dh = th // 2
    items = range(-dh, dh + (th % 2))
    for delta in items:
        antialiased_1px_line(buf, width, height, (x1, y1 + delta), (x2, y2 + delta))


def cross_line(buf: BufType, width: int, height: int, left: bool = True, level: int = 1) -> None:
    if left:
        p1, p2 = (0, 0), (width - 1, height - 1)
    else:
        p1, p2 = (width - 1, 0), (0, height - 1)
    antialiased_line(buf, width, height, p1, p2, level=level)


def half_cross_line(buf: BufType, width: int, height: int, which: str = 'tl', level: int = 1) -> None:
    my = (height - 1) // 2
    if which == 'tl':
        p1 = 0, 0
        p2 = width - 1, my
    elif which == 'bl':
        p2 = 0, height - 1
        p1 = width - 1, my
    elif which == 'tr':
        p1 = width - 1, 0
        p2 = 0, my
    else:
        p2 = width - 1, height - 1
        p1 = 0, my
    antialiased_line(buf, width, height, p1, p2, level=level)


BezierFunc = Callable[[float], float]


def cubic_bezier(start: Tuple[int, int], end: Tuple[int, int], c1: Tuple[int, int], c2: Tuple[int, int]) -> Tuple[BezierFunc, BezierFunc]:

    def bezier_eq(p0: int, p1: int, p2: int, p3: int) -> BezierFunc:

        def f(t: float) -> float:
            tm1 = 1 - t
            tm1_3 = tm1 * tm1 * tm1
            t_3 = t * t * t
            return tm1_3 * p0 + 3 * t * tm1 * (tm1 * p1 + t * p2) + t_3 * p3
        return f

    bezier_x = bezier_eq(start[0], c1[0], c2[0], end[0])
    bezier_y = bezier_eq(start[1], c1[1], c2[1], end[1])
    return bezier_x, bezier_y


def find_bezier_for_D(width: int, height: int) -> int:
    cx = last_cx = width - 1
    start = (0, 0)
    end = (0, height - 1)
    while True:
        c1 = cx, start[1]
        c2 = cx, end[1]
        bezier_x, bezier_y = cubic_bezier(start, end, c1, c2)
        if bezier_x(0.5) > width - 1:
            return last_cx
        last_cx = cx
        cx += 1


def get_bezier_limits(bezier_x: BezierFunc, bezier_y: BezierFunc) -> Generator[Tuple[float, float], None, int]:
    start_x = int(bezier_x(0))
    max_x = int(bezier_x(0.5))
    last_t, t_limit = 0., 0.5

    def find_t_for_x(x: int, start_t: float) -> float:
        if abs(bezier_x(start_t) - x) < 0.1:
            return start_t
        increment = t_limit - start_t
        if increment <= 0:
            return start_t
        while True:
            q = bezier_x(start_t + increment)
            if (abs(q - x) < 0.1):
                return start_t + increment
            if q > x:
                increment /= 2
                if increment < 1e-6:
                    raise ValueError('Failed to find t for x={}'.format(x))
            else:
                start_t += increment
                increment = t_limit - start_t
                if increment <= 0:
                    return start_t

    for x in range(start_x, max_x + 1):
        if x > start_x:
            last_t = find_t_for_x(x, last_t)
        upper, lower = bezier_y(last_t), bezier_y(1 - last_t)
        if abs(upper - lower) <= 2:  # avoid pip on end of D
            break
        yield upper, lower


def D(buf: BufType, width: int, height: int, left: bool = True) -> None:
    c1x = find_bezier_for_D(width, height)
    start = (0, 0)
    end = (0, height - 1)
    c1 = c1x, start[1]
    c2 = c1x, end[1]
    bezier_x, bezier_y = cubic_bezier(start, end, c1, c2)
    xlimits = list(get_bezier_limits(bezier_x, bezier_y))
    if left:
        fill_region(buf, width, height, xlimits)
    else:
        mbuf = bytearray(width * height)
        fill_region(mbuf, width, height, xlimits)
        for y in range(height):
            offset = y * width
            for src_x in range(width):
                dest_x = width - 1 - src_x
                buf[offset + dest_x] = mbuf[offset + src_x]


def half_dhline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'left', only: Optional[str] = None) -> Tuple[int, int]:
    x1, x2 = (0, width // 2) if which == 'left' else (width // 2, width)
    gap = thickness(level + 1, horizontal=False)
    if only != 'bottom':
        draw_hline(buf, width, x1, x2, height // 2 - gap, level)
    if only != 'top':
        draw_hline(buf, width, x1, x2, height // 2 + gap, level)
    return height // 2 - gap, height // 2 + gap


def half_dvline(buf: BufType, width: int, height: int, level: int = 1, which: str = 'top', only: Optional[str] = None) -> Tuple[int, int]:
    y1, y2 = (0, height // 2) if which == 'top' else (height // 2, height)
    gap = thickness(level + 1, horizontal=True)
    if only != 'right':
        draw_vline(buf, width, y1, y2, width // 2 - gap, level)
    if only != 'left':
        draw_vline(buf, width, y1, y2, width // 2 + gap, level)
    return width // 2 - gap, width // 2 + gap


def dvline(buf: BufType, width: int, height: int, only: Optional[str] = None, level: int = 1) -> Tuple[int, int]:
    half_dvline(buf, width, height, only=only, level=level)
    return half_dvline(buf, width, height, only=only, which='bottom', level=level)


def dhline(buf: BufType, width: int, height: int, only: Optional[str] = None, level: int = 1) -> Tuple[int, int]:
    half_dhline(buf, width, height, only=only, level=level)
    return half_dhline(buf, width, height, only=only, which='bottom', level=level)


def dvcorner(buf: BufType, width: int, height: int, level: int = 1, which: str = '╒') -> None:
    hw = 'right' if which in '╒╘' else 'left'
    half_dhline(buf, width, height, which=hw)
    vw = 'top' if which in '╘╛' else 'bottom'
    gap = thickness(level + 1, horizontal=False)
    half_vline(buf, width, height, which=vw, extend_by=gap // 2 + thickness(level, horizontal=False))


def dhcorner(buf: BufType, width: int, height: int, level: int = 1, which: str = '╓') -> None:
    vw = 'top' if which in '╙╜' else 'bottom'
    half_dvline(buf, width, height, which=vw)
    hw = 'right' if which in '╓╙' else 'left'
    gap = thickness(level + 1, horizontal=True)
    half_hline(buf, width, height, which=hw, extend_by=gap // 2 + thickness(level, horizontal=True))


def dcorner(buf: BufType, width: int, height: int, level: int = 1, which: str = '╔') -> None:
    hw = 'right' if which in '╔╚' else 'left'
    vw = 'top' if which in '╚╝' else 'bottom'
    hgap = thickness(level + 1, horizontal=False)
    vgap = thickness(level + 1, horizontal=True)
    x1, x2 = (0, width // 2) if hw == 'left' else (width // 2, width)
    ydelta = hgap if vw == 'top' else -hgap
    if hw == 'left':
        x2 += vgap
    else:
        x1 -= vgap
    draw_hline(buf, width, x1, x2, height // 2 + ydelta, level)
    if hw == 'left':
        x2 -= 2 * vgap
    else:
        x1 += 2 * vgap
    draw_hline(buf, width, x1, x2, height // 2 - ydelta, level)
    y1, y2 = (0, height // 2) if vw == 'top' else (height // 2, height)
    xdelta = vgap if hw == 'right' else -vgap
    yd = thickness(level, horizontal=True) // 2
    if vw == 'top':
        y2 += hgap + yd
    else:
        y1 -= hgap + yd
    draw_vline(buf, width, y1, y2, width // 2 - xdelta, level)
    if vw == 'top':
        y2 -= 2 * hgap
    else:
        y1 += 2 * hgap
    draw_vline(buf, width, y1, y2, width // 2 + xdelta, level)


def dpip(buf: BufType, width: int, height: int, level: int = 1, which: str = '╟') -> None:
    if which in '╟╢':
        left, right = dvline(buf, width, height)
        x1, x2 = (0, left) if which == '╢' else (right, width)
        draw_hline(buf, width, x1, x2, height // 2, level)
    else:
        top, bottom = dhline(buf, width, height)
        y1, y2 = (0, top) if which == '╧' else (bottom, height)
        draw_vline(buf, width, y1, y2, width // 2, level)


def inner_corner(buf: BufType, width: int, height: int, which: str = 'tl', level: int = 1) -> None:
    hgap = thickness(level + 1, horizontal=True)
    vgap = thickness(level + 1, horizontal=False)
    vthick = thickness(level, horizontal=True) // 2
    x1, x2 = (0, width // 2 - hgap + vthick + 1) if 'l' in which else (width // 2 + hgap - vthick, width)
    yd = -1 if 't' in which else 1
    draw_hline(buf, width, x1, x2, height // 2 + (yd * vgap), level)
    y1, y2 = (0, height // 2 - vgap) if 't' in which else (height // 2 + vgap, height)
    xd = -1 if 'l' in which else 1
    draw_vline(buf, width, y1, y2, width // 2 + (xd * hgap), level)


def vblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'top') -> None:
    num_rows = min(height, round(frac * height))
    start = 0 if gravity == 'top' else height - num_rows
    for r in range(start, start + num_rows):
        off = r * width
        for c in range(off, off + width):
            buf[c] = 255


def hblock(buf: BufType, width: int, height: int, frac: float = 1., gravity: str = 'left') -> None:
    num_cols = min(width, round(frac * width))
    start = 0 if gravity == 'left' else width - num_cols
    for r in range(height):
        off = r * width + start
        for c in range(off, off + num_cols):
            buf[c] = 255


def shade(buf: BufType, width: int, height: int, light: bool = False, invert: bool = False) -> None:
    square_sz = max(1, width // 12)
    number_of_rows = height // square_sz
    number_of_cols = width // square_sz
    nums = tuple(range(square_sz))

    dest = bytearray(width * height) if invert else buf

    for r in range(number_of_rows):
        y = r * square_sz
        is_odd = r % 2 != 0
        if is_odd:
            continue
        fill_even = r % 4 == 0
        for yr in nums:
            y = r * square_sz + yr
            if y >= height:
                break
            off = width * y
            for c in range(number_of_cols):
                if light:
                    fill = (c % 4) == (0 if fill_even else 2)
                else:
                    fill = (c % 2 == 0) == fill_even
                if fill:
                    for xc in nums:
                        x = (c * square_sz) + xc
                        if x >= width:
                            break
                        dest[off + x] = 255
    if invert:
        for y in range(height):
            off = width * y
            for x in range(width):
                q = off + x
                buf[q] = 255 - dest[q]


def quad(buf: BufType, width: int, height: int, x: int = 0, y: int = 0) -> None:
    num_cols = width // 2
    left = x * num_cols
    right = width if x else num_cols
    num_rows = height // 2
    top = y * num_rows
    bottom = height if y else num_rows
    for r in range(top, bottom):
        off = r * width
        for c in range(left, right):
            buf[off + c] = 255


box_chars: Dict[str, List[Callable]] = {
    '─': [hline],
    '━': [p(hline, level=3)],
    '│': [vline],
    '┃': [p(vline, level=3)],
    '╌': [hholes],
    '╍': [p(hholes, level=3)],
    '┄': [p(hholes, num=2)],
    '┅': [p(hholes, num=2, level=3)],
    '┈': [p(hholes, num=3)],
    '┉': [p(hholes, num=3, level=3)],
    '╎': [vholes],
    '╏': [p(vholes, level=3)],
    '┆': [p(vholes, num=2)],
    '┇': [p(vholes, num=2, level=3)],
    '┊': [p(vholes, num=3)],
    '┋': [p(vholes, num=3, level=3)],
    '╴': [half_hline],
    '╵': [half_vline],
    '╶': [p(half_hline, which='right')],
    '╷': [p(half_vline, which='bottom')],
    '╸': [p(half_hline, level=3)],
    '╹': [p(half_vline, level=3)],
    '╺': [p(half_hline, which='right', level=3)],
    '╻': [p(half_vline, which='bottom', level=3)],
    '╼': [half_hline, p(half_hline, level=3, which='right')],
    '╽': [half_vline, p(half_vline, level=3, which='bottom')],
    '╾': [p(half_hline, level=3), p(half_hline, which='right')],
    '╿': [p(half_vline, level=3), p(half_vline, which='bottom')],
    '': [triangle],
    '': [p(triangle, left=False)],
    '': [D],
    '': [p(D, left=False)],
    '': [p(half_cross_line, which='tl'), p(half_cross_line, which='bl')],
    '': [p(half_cross_line, which='tr'), p(half_cross_line, which='br')],
    '': [p(corner_triangle, corner='bottom-left')],
    '': [p(corner_triangle, corner='bottom-right')],
    '': [p(corner_triangle, corner='top-left')],
    '': [p(corner_triangle, corner='top-right')],
    '═': [dhline],
    '║': [dvline],
    '╞': [vline, p(half_dhline, which='right')],
    '╡': [vline, half_dhline],
    '╥': [hline, p(half_dvline, which='bottom')],
    '╨': [hline, half_dvline],
    '╪': [vline, half_dhline, p(half_dhline, which='right')],
    '╫': [hline, half_dvline, p(half_dvline, which='bottom')],
    '╬': [p(inner_corner, which=x) for x in 'tl tr bl br'.split()],
    '╠': [p(inner_corner, which='tr'), p(inner_corner, which='br'), p(dvline, only='left')],
    '╣': [p(inner_corner, which='tl'), p(inner_corner, which='bl'), p(dvline, only='right')],
    '╦': [p(inner_corner, which='bl'), p(inner_corner, which='br'), p(dhline, only='top')],
    '╩': [p(inner_corner, which='tl'), p(inner_corner, which='tr'), p(dhline, only='bottom')],
    '╱': [p(cross_line, left=False)],
    '╲': [cross_line],
    '╳': [cross_line, p(cross_line, left=False)],
    '▀': [p(vblock, frac=1/2)],
    '▁': [p(vblock, frac=1/8, gravity='bottom')],
    '▂': [p(vblock, frac=1/4, gravity='bottom')],
    '▃': [p(vblock, frac=3/8, gravity='bottom')],
    '▄': [p(vblock, frac=1/2, gravity='bottom')],
    '▅': [p(vblock, frac=5/8, gravity='bottom')],
    '▆': [p(vblock, frac=3/4, gravity='bottom')],
    '▇': [p(vblock, frac=7/8, gravity='bottom')],
    '█': [p(vblock, frac=1, gravity='bottom')],
    '▉': [p(hblock, frac=7/8)],
    '▊': [p(hblock, frac=3/4)],
    '▋': [p(hblock, frac=5/8)],
    '▌': [p(hblock, frac=1/2)],
    '▍': [p(hblock, frac=3/8)],
    '▎': [p(hblock, frac=1/4)],
    '▏': [p(hblock, frac=1/8)],
    '▐': [p(hblock, frac=1/2, gravity='right')],
    '░': [p(shade, light=True)],
    '▒': [shade],
    '▓': [p(shade, invert=True)],
    '▔': [p(vblock, frac=1/8)],
    '▕': [p(hblock, frac=1/8, gravity='right')],
    '▖': [p(quad, y=1)],
    '▗': [p(quad, x=1, y=1)],
    '▘': [quad],
    '▙': [quad, p(quad, y=1), p(quad, x=1, y=1)],
    '▚': [quad, p(quad, x=1, y=1)],
    '▛': [quad, p(quad, x=1), p(quad, y=1)],
    '▜': [quad, p(quad, x=1, y=1), p(quad, x=1)],
    '▝': [p(quad, x=1)],
    '▞': [p(quad, x=1), p(quad, y=1)],
    '▟': [p(quad, x=1), p(quad, y=1), p(quad, x=1, y=1)],
}

t, f = 1, 3
for start in '┌┐└┘':
    for i, (hlevel, vlevel) in enumerate(((t, t), (f, t), (t, f), (f, f))):
        box_chars[chr(ord(start) + i)] = [p(corner, which=start, hlevel=hlevel, vlevel=vlevel)]
for ch, c in zip('╭╮╯╰', '┌┐┘└'):
    box_chars[ch] = [p(corner, which=c)]  # TODO: Make these rounded

for i, (a_, b_, c_, d_) in enumerate((
        (t, t, t, t), (f, t, t, t), (t, f, t, t), (f, f, t, t), (t, t, f, t), (t, t, t, f), (t, t, f, f),
        (f, t, f, t), (t, f, f, t), (f, t, t, f), (t, f, t, f), (f, f, f, t), (f, f, t, f), (f, t, f, f),
        (t, f, f, f), (f, f, f, f)
)):
    box_chars[chr(ord('┼') + i)] = [p(cross, a=a_, b=b_, c=c_, d=d_)]

for starts, func, pattern in (
        ('├┤', vert_t, ((t, t, t), (t, f, t), (f, t, t), (t, t, f), (f, t, f), (f, f, t), (t, f, f), (f, f, f))),
        ('┬┴', horz_t, ((t, t, t), (f, t, t), (t, f, t), (f, f, t), (t, t, f), (f, t, f), (t, f, f), (f, f, f))),
):
    for start in starts:
        for i, (a_, b_, c_) in enumerate(pattern):
            box_chars[chr(ord(start) + i)] = [p(func, which=start, a=a_, b=b_, c=c_)]

for chars, func_ in (('╒╕╘╛', dvcorner), ('╓╖╙╜', dhcorner), ('╔╗╚╝', dcorner), ('╟╢╤╧', dpip)):
    for ch in chars:
        box_chars[ch] = [p(cast(Callable, func_), which=ch)]


def render_box_char(ch: str, buf: BufType, width: int, height: int, dpi: float = 96.0) -> BufType:
    global _dpi
    _dpi = dpi
    for func in box_chars[ch]:
        func(buf, width, height)
    return buf


def render_missing_glyph(buf: BufType, width: int, height: int) -> None:
    hgap = thickness(level=0, horizontal=True) + 1
    vgap = thickness(level=0, horizontal=False) + 1
    draw_hline(buf, width, hgap, width - hgap + 1, vgap, 0)
    draw_hline(buf, width, hgap, width - hgap + 1, height - vgap, 0)
    draw_vline(buf, width, vgap, height - vgap + 1, hgap, 0)
    draw_vline(buf, width, vgap, height - vgap + 1, width - hgap, 0)


def test_char(ch: str, sz: int = 48) -> None:
    # kitty +runpy "from kitty.fonts.box_drawing import test_char; test_char('XXX')"
    from .render import display_bitmap, setup_for_testing
    from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu
    with setup_for_testing('monospace', sz) as (_, width, height):
        buf = bytearray(width * height)
        try:
            render_box_char(ch, buf, width, height)

            def join_cells(*cells: bytes) -> bytes:
                cells = tuple(bytes(x) for x in cells)
                return concat_cells(width, height, False, cells)

            rgb_data = join_cells(buf)
            display_bitmap(rgb_data, width, height)
            print()
        finally:
            set_send_sprite_to_gpu(None)


def test_drawing(sz: int = 48, family: str = 'monospace') -> None:
    from .render import display_bitmap, setup_for_testing
    from kitty.fast_data_types import concat_cells, set_send_sprite_to_gpu

    with setup_for_testing(family, sz) as (_, width, height):
        space = bytearray(width * height)

        def join_cells(cells: Iterable[bytes]) -> bytes:
            cells = tuple(bytes(x) for x in cells)
            return concat_cells(width, height, False, cells)

        def render_chr(ch: str) -> bytearray:
            if ch in box_chars:
                cell = bytearray(len(space))
                render_box_char(ch, cell, width, height)
                return cell
            return space

        pos = 0x2500
        rows = []
        space_row = join_cells(repeat(space, 32))

        try:
            for r in range(10):
                row = []
                for i in range(16):
                    row.append(render_chr(chr(pos)))
                    row.append(space)
                    pos += 1
                rows.append(join_cells(row))
                rows.append(space_row)
            rgb_data = b''.join(rows)
            width *= 32
            height *= len(rows)
            assert len(rgb_data) == width * height * 4, '{} != {}'.format(len(rgb_data), width * height * 4)
            display_bitmap(rgb_data, width, height)
        finally:
            set_send_sprite_to_gpu(None)
