//! Box Drawing | U+2500...U+257F
//! https://en.wikipedia.org/wiki/Box_Drawing
//!
//! ─━│┃┄┅┆┇┈┉┊┋┌┍┎┏
//! ┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟
//! ┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯
//! ┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿
//! ╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏
//! ═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟
//! ╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯
//! ╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿
//!

const std = @import("std");
const assert = @import("../../../quirks.zig").inlineAssert;
const Allocator = std.mem.Allocator;

const common = @import("common.zig");
const Thickness = common.Thickness;
const Shade = common.Shade;
const Quads = common.Quads;
const Corner = common.Corner;
const Edge = common.Edge;
const Alignment = common.Alignment;
const hline = common.hline;
const vline = common.vline;
const hlineMiddle = common.hlineMiddle;
const vlineMiddle = common.vlineMiddle;

const font = @import("../../main.zig");

/// Specification of a traditional intersection-style line/box-drawing char,
/// which can have a different style of line from each edge to the center.
pub const Lines = packed struct(u8) {
    up: Style = .none,
    right: Style = .none,
    down: Style = .none,
    left: Style = .none,

    const Style = enum(u2) {
        none,
        light,
        heavy,
        double,
    };
};

pub fn draw2500_257F(
    cp: u32,
    canvas: *font.sprite.Canvas,
    width: u32,
    height: u32,
    metrics: font.Metrics,
) !void {
    _ = width;
    _ = height;

    switch (cp) {
        // '─'
        0x2500 => linesChar(metrics, canvas, .{ .left = .light, .right = .light }),
        // '━'
        0x2501 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .heavy }),
        // '│'
        0x2502 => linesChar(metrics, canvas, .{ .up = .light, .down = .light }),
        // '┃'
        0x2503 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy }),
        // '┄'
        0x2504 => dashHorizontal(
            metrics,
            canvas,
            3,
            Thickness.light.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┅'
        0x2505 => dashHorizontal(
            metrics,
            canvas,
            3,
            Thickness.heavy.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┆'
        0x2506 => dashVertical(
            metrics,
            canvas,
            3,
            Thickness.light.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┇'
        0x2507 => dashVertical(
            metrics,
            canvas,
            3,
            Thickness.heavy.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┈'
        0x2508 => dashHorizontal(
            metrics,
            canvas,
            4,
            Thickness.light.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┉'
        0x2509 => dashHorizontal(
            metrics,
            canvas,
            4,
            Thickness.heavy.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┊'
        0x250a => dashVertical(
            metrics,
            canvas,
            4,
            Thickness.light.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┋'
        0x250b => dashVertical(
            metrics,
            canvas,
            4,
            Thickness.heavy.height(metrics.box_thickness),
            @max(4, Thickness.light.height(metrics.box_thickness)),
        ),
        // '┌'
        0x250c => linesChar(metrics, canvas, .{ .down = .light, .right = .light }),
        // '┍'
        0x250d => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy }),
        // '┎'
        0x250e => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light }),
        // '┏'
        0x250f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .heavy }),

        // '┐'
        0x2510 => linesChar(metrics, canvas, .{ .down = .light, .left = .light }),
        // '┑'
        0x2511 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy }),
        // '┒'
        0x2512 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light }),
        // '┓'
        0x2513 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy }),
        // '└'
        0x2514 => linesChar(metrics, canvas, .{ .up = .light, .right = .light }),
        // '┕'
        0x2515 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy }),
        // '┖'
        0x2516 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light }),
        // '┗'
        0x2517 => linesChar(metrics, canvas, .{ .up = .heavy, .right = .heavy }),
        // '┘'
        0x2518 => linesChar(metrics, canvas, .{ .up = .light, .left = .light }),
        // '┙'
        0x2519 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy }),
        // '┚'
        0x251a => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light }),
        // '┛'
        0x251b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy }),
        // '├'
        0x251c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .light }),
        // '┝'
        0x251d => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .heavy }),
        // '┞'
        0x251e => linesChar(metrics, canvas, .{ .up = .heavy, .right = .light, .down = .light }),
        // '┟'
        0x251f => linesChar(metrics, canvas, .{ .down = .heavy, .right = .light, .up = .light }),

        // '┠'
        0x2520 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .light }),
        // '┡'
        0x2521 => linesChar(metrics, canvas, .{ .down = .light, .right = .heavy, .up = .heavy }),
        // '┢'
        0x2522 => linesChar(metrics, canvas, .{ .up = .light, .right = .heavy, .down = .heavy }),
        // '┣'
        0x2523 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .right = .heavy }),
        // '┤'
        0x2524 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light }),
        // '┥'
        0x2525 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy }),
        // '┦'
        0x2526 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .down = .light }),
        // '┧'
        0x2527 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .up = .light }),
        // '┨'
        0x2528 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light }),
        // '┩'
        0x2529 => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .up = .heavy }),
        // '┪'
        0x252a => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .down = .heavy }),
        // '┫'
        0x252b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy }),
        // '┬'
        0x252c => linesChar(metrics, canvas, .{ .down = .light, .left = .light, .right = .light }),
        // '┭'
        0x252d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .down = .light }),
        // '┮'
        0x252e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .down = .light }),
        // '┯'
        0x252f => linesChar(metrics, canvas, .{ .down = .light, .left = .heavy, .right = .heavy }),

        // '┰'
        0x2530 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .light, .right = .light }),
        // '┱'
        0x2531 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .down = .heavy }),
        // '┲'
        0x2532 => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .down = .heavy }),
        // '┳'
        0x2533 => linesChar(metrics, canvas, .{ .down = .heavy, .left = .heavy, .right = .heavy }),
        // '┴'
        0x2534 => linesChar(metrics, canvas, .{ .up = .light, .left = .light, .right = .light }),
        // '┵'
        0x2535 => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light }),
        // '┶'
        0x2536 => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light }),
        // '┷'
        0x2537 => linesChar(metrics, canvas, .{ .up = .light, .left = .heavy, .right = .heavy }),
        // '┸'
        0x2538 => linesChar(metrics, canvas, .{ .up = .heavy, .left = .light, .right = .light }),
        // '┹'
        0x2539 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy }),
        // '┺'
        0x253a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy }),
        // '┻'
        0x253b => linesChar(metrics, canvas, .{ .up = .heavy, .left = .heavy, .right = .heavy }),
        // '┼'
        0x253c => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .light, .right = .light }),
        // '┽'
        0x253d => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light, .up = .light, .down = .light }),
        // '┾'
        0x253e => linesChar(metrics, canvas, .{ .right = .heavy, .left = .light, .up = .light, .down = .light }),
        // '┿'
        0x253f => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .heavy, .right = .heavy }),

        // '╀'
        0x2540 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light, .left = .light, .right = .light }),
        // '╁'
        0x2541 => linesChar(metrics, canvas, .{ .down = .heavy, .up = .light, .left = .light, .right = .light }),
        // '╂'
        0x2542 => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .light, .right = .light }),
        // '╃'
        0x2543 => linesChar(metrics, canvas, .{ .left = .heavy, .up = .heavy, .right = .light, .down = .light }),
        // '╄'
        0x2544 => linesChar(metrics, canvas, .{ .right = .heavy, .up = .heavy, .left = .light, .down = .light }),
        // '╅'
        0x2545 => linesChar(metrics, canvas, .{ .left = .heavy, .down = .heavy, .right = .light, .up = .light }),
        // '╆'
        0x2546 => linesChar(metrics, canvas, .{ .right = .heavy, .down = .heavy, .left = .light, .up = .light }),
        // '╇'
        0x2547 => linesChar(metrics, canvas, .{ .down = .light, .up = .heavy, .left = .heavy, .right = .heavy }),
        // '╈'
        0x2548 => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy, .left = .heavy, .right = .heavy }),
        // '╉'
        0x2549 => linesChar(metrics, canvas, .{ .right = .light, .left = .heavy, .up = .heavy, .down = .heavy }),
        // '╊'
        0x254a => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy, .up = .heavy, .down = .heavy }),
        // '╋'
        0x254b => linesChar(metrics, canvas, .{ .up = .heavy, .down = .heavy, .left = .heavy, .right = .heavy }),
        // '╌'
        0x254c => dashHorizontal(
            metrics,
            canvas,
            2,
            Thickness.light.height(metrics.box_thickness),
            Thickness.light.height(metrics.box_thickness),
        ),
        // '╍'
        0x254d => dashHorizontal(
            metrics,
            canvas,
            2,
            Thickness.heavy.height(metrics.box_thickness),
            Thickness.heavy.height(metrics.box_thickness),
        ),
        // '╎'
        0x254e => dashVertical(
            metrics,
            canvas,
            2,
            Thickness.light.height(metrics.box_thickness),
            Thickness.heavy.height(metrics.box_thickness),
        ),
        // '╏'
        0x254f => dashVertical(
            metrics,
            canvas,
            2,
            Thickness.heavy.height(metrics.box_thickness),
            Thickness.heavy.height(metrics.box_thickness),
        ),

        // '═'
        0x2550 => linesChar(metrics, canvas, .{ .left = .double, .right = .double }),
        // '║'
        0x2551 => linesChar(metrics, canvas, .{ .up = .double, .down = .double }),
        // '╒'
        0x2552 => linesChar(metrics, canvas, .{ .down = .light, .right = .double }),
        // '╓'
        0x2553 => linesChar(metrics, canvas, .{ .down = .double, .right = .light }),
        // '╔'
        0x2554 => linesChar(metrics, canvas, .{ .down = .double, .right = .double }),
        // '╕'
        0x2555 => linesChar(metrics, canvas, .{ .down = .light, .left = .double }),
        // '╖'
        0x2556 => linesChar(metrics, canvas, .{ .down = .double, .left = .light }),
        // '╗'
        0x2557 => linesChar(metrics, canvas, .{ .down = .double, .left = .double }),
        // '╘'
        0x2558 => linesChar(metrics, canvas, .{ .up = .light, .right = .double }),
        // '╙'
        0x2559 => linesChar(metrics, canvas, .{ .up = .double, .right = .light }),
        // '╚'
        0x255a => linesChar(metrics, canvas, .{ .up = .double, .right = .double }),
        // '╛'
        0x255b => linesChar(metrics, canvas, .{ .up = .light, .left = .double }),
        // '╜'
        0x255c => linesChar(metrics, canvas, .{ .up = .double, .left = .light }),
        // '╝'
        0x255d => linesChar(metrics, canvas, .{ .up = .double, .left = .double }),
        // '╞'
        0x255e => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .right = .double }),
        // '╟'
        0x255f => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .light }),

        // '╠'
        0x2560 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .right = .double }),
        // '╡'
        0x2561 => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double }),
        // '╢'
        0x2562 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light }),
        // '╣'
        0x2563 => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double }),
        // '╤'
        0x2564 => linesChar(metrics, canvas, .{ .down = .light, .left = .double, .right = .double }),
        // '╥'
        0x2565 => linesChar(metrics, canvas, .{ .down = .double, .left = .light, .right = .light }),
        // '╦'
        0x2566 => linesChar(metrics, canvas, .{ .down = .double, .left = .double, .right = .double }),
        // '╧'
        0x2567 => linesChar(metrics, canvas, .{ .up = .light, .left = .double, .right = .double }),
        // '╨'
        0x2568 => linesChar(metrics, canvas, .{ .up = .double, .left = .light, .right = .light }),
        // '╩'
        0x2569 => linesChar(metrics, canvas, .{ .up = .double, .left = .double, .right = .double }),
        // '╪'
        0x256a => linesChar(metrics, canvas, .{ .up = .light, .down = .light, .left = .double, .right = .double }),
        // '╫'
        0x256b => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .light, .right = .light }),
        // '╬'
        0x256c => linesChar(metrics, canvas, .{ .up = .double, .down = .double, .left = .double, .right = .double }),
        // '╭'
        0x256d => try arc(metrics, canvas, .br, .light),
        // '╮'
        0x256e => try arc(metrics, canvas, .bl, .light),
        // '╯'
        0x256f => try arc(metrics, canvas, .tl, .light),

        // '╰'
        0x2570 => try arc(metrics, canvas, .tr, .light),
        // '╱'
        0x2571 => lightDiagonalUpperRightToLowerLeft(metrics, canvas),
        // '╲'
        0x2572 => lightDiagonalUpperLeftToLowerRight(metrics, canvas),
        // '╳'
        0x2573 => lightDiagonalCross(metrics, canvas),
        // '╴'
        0x2574 => linesChar(metrics, canvas, .{ .left = .light }),
        // '╵'
        0x2575 => linesChar(metrics, canvas, .{ .up = .light }),
        // '╶'
        0x2576 => linesChar(metrics, canvas, .{ .right = .light }),
        // '╷'
        0x2577 => linesChar(metrics, canvas, .{ .down = .light }),
        // '╸'
        0x2578 => linesChar(metrics, canvas, .{ .left = .heavy }),
        // '╹'
        0x2579 => linesChar(metrics, canvas, .{ .up = .heavy }),
        // '╺'
        0x257a => linesChar(metrics, canvas, .{ .right = .heavy }),
        // '╻'
        0x257b => linesChar(metrics, canvas, .{ .down = .heavy }),
        // '╼'
        0x257c => linesChar(metrics, canvas, .{ .left = .light, .right = .heavy }),
        // '╽'
        0x257d => linesChar(metrics, canvas, .{ .up = .light, .down = .heavy }),
        // '╾'
        0x257e => linesChar(metrics, canvas, .{ .left = .heavy, .right = .light }),
        // '╿'
        0x257f => linesChar(metrics, canvas, .{ .up = .heavy, .down = .light }),

        else => unreachable,
    }
}

pub fn linesChar(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
    lines: Lines,
) void {
    const light_px = Thickness.light.height(metrics.box_thickness);
    const heavy_px = Thickness.heavy.height(metrics.box_thickness);

    // Top of light horizontal strokes
    const h_light_top = (metrics.cell_height -| light_px) / 2;
    // Bottom of light horizontal strokes
    const h_light_bottom = h_light_top +| light_px;

    // Top of heavy horizontal strokes
    const h_heavy_top = (metrics.cell_height -| heavy_px) / 2;
    // Bottom of heavy horizontal strokes
    const h_heavy_bottom = h_heavy_top +| heavy_px;

    // Top of the top doubled horizontal stroke (bottom is `h_light_top`)
    const h_double_top = h_light_top -| light_px;
    // Bottom of the bottom doubled horizontal stroke (top is `h_light_bottom`)
    const h_double_bottom = h_light_bottom +| light_px;

    // Left of light vertical strokes
    const v_light_left = (metrics.cell_width -| light_px) / 2;
    // Right of light vertical strokes
    const v_light_right = v_light_left +| light_px;

    // Left of heavy vertical strokes
    const v_heavy_left = (metrics.cell_width -| heavy_px) / 2;
    // Right of heavy vertical strokes
    const v_heavy_right = v_heavy_left +| heavy_px;

    // Left of the left doubled vertical stroke (right is `v_light_left`)
    const v_double_left = v_light_left -| light_px;
    // Right of the right doubled vertical stroke (left is `v_light_right`)
    const v_double_right = v_light_right +| light_px;

    // The bottom of the up line
    const up_bottom = if (lines.left == .heavy or lines.right == .heavy)
        h_heavy_bottom
    else if (lines.left != lines.right or lines.down == lines.up)
        if (lines.left == .double or lines.right == .double)
            h_double_bottom
        else
            h_light_bottom
    else if (lines.left == .none and lines.right == .none)
        h_light_bottom
    else
        h_light_top;

    // The top of the down line
    const down_top = if (lines.left == .heavy or lines.right == .heavy)
        h_heavy_top
    else if (lines.left != lines.right or lines.up == lines.down)
        if (lines.left == .double or lines.right == .double)
            h_double_top
        else
            h_light_top
    else if (lines.left == .none and lines.right == .none)
        h_light_top
    else
        h_light_bottom;

    // The right of the left line
    const left_right = if (lines.up == .heavy or lines.down == .heavy)
        v_heavy_right
    else if (lines.up != lines.down or lines.left == lines.right)
        if (lines.up == .double or lines.down == .double)
            v_double_right
        else
            v_light_right
    else if (lines.up == .none and lines.down == .none)
        v_light_right
    else
        v_light_left;

    // The left of the right line
    const right_left = if (lines.up == .heavy or lines.down == .heavy)
        v_heavy_left
    else if (lines.up != lines.down or lines.right == lines.left)
        if (lines.up == .double or lines.down == .double)
            v_double_left
        else
            v_light_left
    else if (lines.up == .none and lines.down == .none)
        v_light_left
    else
        v_light_right;

    switch (lines.up) {
        .none => {},
        .light => canvas.box(
            @intCast(v_light_left),
            0,
            @intCast(v_light_right),
            @intCast(up_bottom),
            .on,
        ),
        .heavy => canvas.box(
            @intCast(v_heavy_left),
            0,
            @intCast(v_heavy_right),
            @intCast(up_bottom),
            .on,
        ),
        .double => {
            const left_bottom = if (lines.left == .double) h_light_top else up_bottom;
            const right_bottom = if (lines.right == .double) h_light_top else up_bottom;

            canvas.box(
                @intCast(v_double_left),
                0,
                @intCast(v_light_left),
                @intCast(left_bottom),
                .on,
            );
            canvas.box(
                @intCast(v_light_right),
                0,
                @intCast(v_double_right),
                @intCast(right_bottom),
                .on,
            );
        },
    }

    switch (lines.right) {
        .none => {},
        .light => canvas.box(
            @intCast(right_left),
            @intCast(h_light_top),
            @intCast(metrics.cell_width),
            @intCast(h_light_bottom),
            .on,
        ),
        .heavy => canvas.box(
            @intCast(right_left),
            @intCast(h_heavy_top),
            @intCast(metrics.cell_width),
            @intCast(h_heavy_bottom),
            .on,
        ),
        .double => {
            const top_left = if (lines.up == .double) v_light_right else right_left;
            const bottom_left = if (lines.down == .double) v_light_right else right_left;

            canvas.box(
                @intCast(top_left),
                @intCast(h_double_top),
                @intCast(metrics.cell_width),
                @intCast(h_light_top),
                .on,
            );
            canvas.box(
                @intCast(bottom_left),
                @intCast(h_light_bottom),
                @intCast(metrics.cell_width),
                @intCast(h_double_bottom),
                .on,
            );
        },
    }

    switch (lines.down) {
        .none => {},
        .light => canvas.box(
            @intCast(v_light_left),
            @intCast(down_top),
            @intCast(v_light_right),
            @intCast(metrics.cell_height),
            .on,
        ),
        .heavy => canvas.box(
            @intCast(v_heavy_left),
            @intCast(down_top),
            @intCast(v_heavy_right),
            @intCast(metrics.cell_height),
            .on,
        ),
        .double => {
            const left_top = if (lines.left == .double) h_light_bottom else down_top;
            const right_top = if (lines.right == .double) h_light_bottom else down_top;

            canvas.box(
                @intCast(v_double_left),
                @intCast(left_top),
                @intCast(v_light_left),
                @intCast(metrics.cell_height),
                .on,
            );
            canvas.box(
                @intCast(v_light_right),
                @intCast(right_top),
                @intCast(v_double_right),
                @intCast(metrics.cell_height),
                .on,
            );
        },
    }

    switch (lines.left) {
        .none => {},
        .light => canvas.box(
            0,
            @intCast(h_light_top),
            @intCast(left_right),
            @intCast(h_light_bottom),
            .on,
        ),
        .heavy => canvas.box(
            0,
            @intCast(h_heavy_top),
            @intCast(left_right),
            @intCast(h_heavy_bottom),
            .on,
        ),
        .double => {
            const top_right = if (lines.up == .double) v_light_left else left_right;
            const bottom_right = if (lines.down == .double) v_light_left else left_right;

            canvas.box(
                0,
                @intCast(h_double_top),
                @intCast(top_right),
                @intCast(h_light_top),
                .on,
            );
            canvas.box(
                0,
                @intCast(h_light_bottom),
                @intCast(bottom_right),
                @intCast(h_double_bottom),
                .on,
            );
        },
    }
}

pub fn lightDiagonalUpperRightToLowerLeft(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
) void {
    const float_width: f64 = @floatFromInt(metrics.cell_width);
    const float_height: f64 = @floatFromInt(metrics.cell_height);

    // We overshoot the corners by a tiny bit, but we need to
    // maintain the correct slope, so we calculate that here.
    const slope_x: f64 = @min(1.0, float_width / float_height);
    const slope_y: f64 = @min(1.0, float_height / float_width);

    canvas.line(.{
        .p0 = .{
            .x = float_width + 0.5 * slope_x,
            .y = -0.5 * slope_y,
        },
        .p1 = .{
            .x = -0.5 * slope_x,
            .y = float_height + 0.5 * slope_y,
        },
    }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {};
}

pub fn lightDiagonalUpperLeftToLowerRight(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
) void {
    const float_width: f64 = @floatFromInt(metrics.cell_width);
    const float_height: f64 = @floatFromInt(metrics.cell_height);

    // We overshoot the corners by a tiny bit, but we need to
    // maintain the correct slope, so we calculate that here.
    const slope_x: f64 = @min(1.0, float_width / float_height);
    const slope_y: f64 = @min(1.0, float_height / float_width);

    canvas.line(.{
        .p0 = .{
            .x = -0.5 * slope_x,
            .y = -0.5 * slope_y,
        },
        .p1 = .{
            .x = float_width + 0.5 * slope_x,
            .y = float_height + 0.5 * slope_y,
        },
    }, @floatFromInt(Thickness.light.height(metrics.box_thickness)), .on) catch {};
}

pub fn lightDiagonalCross(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
) void {
    lightDiagonalUpperRightToLowerLeft(metrics, canvas);
    lightDiagonalUpperLeftToLowerRight(metrics, canvas);
}

pub fn arc(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
    comptime corner: Corner,
    comptime thickness: Thickness,
) !void {
    const thick_px = thickness.height(metrics.box_thickness);
    const float_width: f64 = @floatFromInt(metrics.cell_width);
    const float_height: f64 = @floatFromInt(metrics.cell_height);
    const float_thick: f64 = @floatFromInt(thick_px);
    const center_x: f64 = @as(f64, @floatFromInt((metrics.cell_width -| thick_px) / 2)) + float_thick / 2;
    const center_y: f64 = @as(f64, @floatFromInt((metrics.cell_height -| thick_px) / 2)) + float_thick / 2;

    const r = @min(float_width, float_height) / 2;

    // Fraction away from the center to place the middle control points,
    const s: f64 = 0.25;

    var path = canvas.staticPath(4);

    switch (corner) {
        .tl => {
            path.moveTo(center_x, 0);
            path.lineTo(center_x, center_y - r);
            path.curveTo(
                center_x,
                center_y - s * r,
                center_x - s * r,
                center_y,
                center_x - r,
                center_y,
            );
            path.lineTo(0, center_y);
        },
        .tr => {
            path.moveTo(center_x, 0);
            path.lineTo(center_x, center_y - r);
            path.curveTo(
                center_x,
                center_y - s * r,
                center_x + s * r,
                center_y,
                center_x + r,
                center_y,
            );
            path.lineTo(float_width, center_y);
        },
        .bl => {
            path.moveTo(center_x, float_height);
            path.lineTo(center_x, center_y + r);
            path.curveTo(
                center_x,
                center_y + s * r,
                center_x - s * r,
                center_y,
                center_x - r,
                center_y,
            );
            path.lineTo(0, center_y);
        },
        .br => {
            path.moveTo(center_x, float_height);
            path.lineTo(center_x, center_y + r);
            path.curveTo(
                center_x,
                center_y + s * r,
                center_x + s * r,
                center_y,
                center_x + r,
                center_y,
            );
            path.lineTo(float_width, center_y);
        },
    }

    try canvas.strokePath(
        path.wrapped_path,
        .{
            .line_cap_mode = .butt,
            .line_width = float_thick,
        },
        .on,
    );
}

fn dashHorizontal(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
    count: u8,
    thick_px: u32,
    desired_gap: u32,
) void {
    assert(count >= 2 and count <= 4);

    // +------------+
    // |            |
    // |            |
    // |            |
    // |            |
    // | --  --  -- |
    // |            |
    // |            |
    // |            |
    // |            |
    // +------------+
    // Our dashed line should be made such that when tiled horizontally
    // it creates one consistent line with no uneven gap or segment sizes.
    // In order to make sure this is the case, we should have half-sized
    // gaps on the left and right so that it is centered properly.

    // For N dashes, there are N - 1 gaps between them, but we also have
    // half-sized gaps on either side, adding up to N total gaps.
    const gap_count = count;

    // We need at least 1 pixel for each gap and each dash, if we don't
    // have that then we can't draw our dashed line correctly so we just
    // draw a solid line and return.
    if (metrics.cell_width < count + gap_count) {
        hlineMiddle(metrics, canvas, .light);
        return;
    }

    // We never want the gaps to take up more than 50% of the space,
    // because if they do the dashes are too small and look wrong.
    const gap_width: i32 = @intCast(@min(desired_gap, metrics.cell_width / (2 * count)));
    const total_gap_width: i32 = gap_count * gap_width;
    const total_dash_width: i32 = @as(i32, @intCast(metrics.cell_width)) - total_gap_width;
    const dash_width: i32 = @divFloor(total_dash_width, count);
    const remaining: i32 = @mod(total_dash_width, count);

    assert(dash_width * count + gap_width * gap_count + remaining == metrics.cell_width);

    // Our dashes should be centered vertically.
    const y: i32 = @intCast((metrics.cell_height -| thick_px) / 2);

    // We start at half a gap from the left edge, in order to center
    // our dashes properly.
    var x: i32 = @divFloor(gap_width, 2);

    // We'll distribute the extra space in to dash widths, 1px at a
    // time. We prefer this to making gaps larger since that is much
    // more visually obvious.
    var extra: i32 = remaining;

    for (0..count) |_| {
        var x1 = x + dash_width;
        // We distribute left-over size in to dash widths,
        // since it's less obvious there than in the gaps.
        if (extra > 0) {
            extra -= 1;
            x1 += 1;
        }
        hline(canvas, x, x1, y, thick_px);
        // Advance by the width of the dash we drew and the width
        // of a gap to get the start of the next dash.
        x = x1 + gap_width;
    }
}

fn dashVertical(
    metrics: font.Metrics,
    canvas: *font.sprite.Canvas,
    comptime count: u8,
    thick_px: u32,
    desired_gap: u32,
) void {
    assert(count >= 2 and count <= 4);

    // +-----------+
    // |     |     |
    // |     |     |
    // |           |
    // |     |     |
    // |     |     |
    // |           |
    // |     |     |
    // |     |     |
    // |           |
    // +-----------+
    // Our dashed line should be made such that when tiled vertically it
    // it creates one consistent line with no uneven gap or segment sizes.
    // In order to make sure this is the case, we should have an extra gap
    // gap at the bottom.
    //
    // A single full-sized extra gap is preferred to two half-sized ones for
    // vertical to allow better joining to solid characters without creating
    // visible half-sized gaps. Unlike horizontal, centering is a lot less
    // important, visually.

    // Because of the extra gap at the bottom, there are as many gaps as
    // there are dashes.
    const gap_count = count;

    // We need at least 1 pixel for each gap and each dash, if we don't
    // have that then we can't draw our dashed line correctly so we just
    // draw a solid line and return.
    if (metrics.cell_height < count + gap_count) {
        vlineMiddle(metrics, canvas, .light);
        return;
    }

    // We never want the gaps to take up more than 50% of the space,
    // because if they do the dashes are too small and look wrong.
    const gap_height: i32 = @intCast(@min(desired_gap, metrics.cell_height / (2 * count)));
    const total_gap_height: i32 = gap_count * gap_height;
    const total_dash_height: i32 = @as(i32, @intCast(metrics.cell_height)) - total_gap_height;
    const dash_height: i32 = @divFloor(total_dash_height, count);
    const remaining: i32 = @mod(total_dash_height, count);

    assert(dash_height * count + gap_height * gap_count + remaining == metrics.cell_height);

    // Our dashes should be centered horizontally.
    const x: i32 = @intCast((metrics.cell_width -| thick_px) / 2);

    // We start at the top of the cell.
    var y: i32 = 0;

    // We'll distribute the extra space in to dash heights, 1px at a
    // time. We prefer this to making gaps larger since that is much
    // more visually obvious.
    var extra: i32 = remaining;

    inline for (0..count) |_| {
        var y1 = y + dash_height;
        // We distribute left-over size in to dash widths,
        // since it's less obvious there than in the gaps.
        if (extra > 0) {
            extra -= 1;
            y1 += 1;
        }
        vline(canvas, y, y1, x, thick_px);
        // Advance by the height of the dash we drew and the height
        // of a gap to get the start of the next dash.
        y = y1 + gap_height;
    }
}
