const std = @import("std");
const terminal = @import("../terminal/main.zig");
const State = @import("State.zig");

/// Available cursor styles for drawing that renderers must support.
/// This is a superset of terminal cursor styles since the renderer supports
/// some additional cursor states such as the hollow block.
pub const CursorStyle = enum {
    block,
    block_hollow,
    bar,
    underline,

    /// Create a cursor style from the terminal style request.
    pub fn fromTerminal(style: terminal.Cursor.Style) ?CursorStyle {
        return switch (style) {
            .bar => .bar,
            .block => .block,
            .underline => .underline,
        };
    }
};

/// Returns the cursor style to use for the current render state or null
/// if a cursor should not be rendered at all.
pub fn cursorStyle(
    state: *State,
    focused: bool,
    blink_visible: bool,
) ?CursorStyle {
    // Note the order of conditionals below is important. It represents
    // a priority system of how we determine what state overrides cursor
    // visibility and style.

    // The cursor is only at the bottom of the viewport. If we aren't
    // at the bottom, we never render the cursor. The cursor x/y is by
    // viewport so if we are above the viewport, we'll end up rendering
    // the cursor in some random part of the screen.
    if (!state.terminal.screen.viewportIsBottom()) return null;

    // If we are in preedit, then we always show the block cursor. We do
    // this even if the cursor is explicitly not visible because it shows
    // an important editing state to the user.
    if (state.preedit != null) return .block;

    // If the cursor is explicitly not visible by terminal mode, we don't render.
    if (!state.terminal.modes.get(.cursor_visible)) return null;

    // If we're not focused, our cursor is always visible so that
    // we can show the hollow box.
    if (!focused) return .block_hollow;

    // If the cursor is blinking and our blink state is not visible,
    // then we don't show the cursor.
    if (state.terminal.modes.get(.cursor_blinking) and !blink_visible) {
        return null;
    }

    // Otherwise, we use whatever style the terminal wants.
    return CursorStyle.fromTerminal(state.terminal.screen.cursor.style);
}

test "cursor: default uses configured style" {
    const testing = std.testing;
    const alloc = testing.allocator;
    var term = try terminal.Terminal.init(alloc, 10, 10);
    defer term.deinit(alloc);

    term.screen.cursor.style = .bar;
    term.modes.set(.cursor_blinking, true);

    var state: State = .{
        .mutex = undefined,
        .terminal = &term,
        .preedit = null,
    };

    try testing.expect(cursorStyle(&state, true, true) == .bar);
    try testing.expect(cursorStyle(&state, false, true) == .block_hollow);
    try testing.expect(cursorStyle(&state, false, false) == .block_hollow);
    try testing.expect(cursorStyle(&state, true, false) == null);
}

test "cursor: blinking disabled" {
    const testing = std.testing;
    const alloc = testing.allocator;
    var term = try terminal.Terminal.init(alloc, 10, 10);
    defer term.deinit(alloc);

    term.screen.cursor.style = .bar;
    term.modes.set(.cursor_blinking, false);

    var state: State = .{
        .mutex = undefined,
        .terminal = &term,
        .preedit = null,
    };

    try testing.expect(cursorStyle(&state, true, true) == .bar);
    try testing.expect(cursorStyle(&state, true, false) == .bar);
    try testing.expect(cursorStyle(&state, false, true) == .block_hollow);
    try testing.expect(cursorStyle(&state, false, false) == .block_hollow);
}

test "cursor: explictly not visible" {
    const testing = std.testing;
    const alloc = testing.allocator;
    var term = try terminal.Terminal.init(alloc, 10, 10);
    defer term.deinit(alloc);

    term.screen.cursor.style = .bar;
    term.modes.set(.cursor_visible, false);
    term.modes.set(.cursor_blinking, false);

    var state: State = .{
        .mutex = undefined,
        .terminal = &term,
        .preedit = null,
    };

    try testing.expect(cursorStyle(&state, true, true) == null);
    try testing.expect(cursorStyle(&state, true, false) == null);
    try testing.expect(cursorStyle(&state, false, true) == null);
    try testing.expect(cursorStyle(&state, false, false) == null);
}

test "cursor: always block with preedit" {
    const testing = std.testing;
    const alloc = testing.allocator;
    var term = try terminal.Terminal.init(alloc, 10, 10);
    defer term.deinit(alloc);

    var state: State = .{
        .mutex = undefined,
        .terminal = &term,
        .preedit = .{},
    };

    // In any bool state
    try testing.expect(cursorStyle(&state, false, false) == .block);
    try testing.expect(cursorStyle(&state, true, false) == .block);
    try testing.expect(cursorStyle(&state, true, true) == .block);
    try testing.expect(cursorStyle(&state, false, true) == .block);

    // If we're scrolled though, then we don't show the cursor.
    for (0..100) |_| try term.index();
    try term.scrollViewport(.{ .top = {} });

    // In any bool state
    try testing.expect(cursorStyle(&state, false, false) == null);
    try testing.expect(cursorStyle(&state, true, false) == null);
    try testing.expect(cursorStyle(&state, true, true) == null);
    try testing.expect(cursorStyle(&state, false, true) == null);
}
