Skip to main content

src/etui_buffer_array_ffi.erl

-module(etui_buffer_array_ffi).
-export([new/2, get/2, set/3, fill_string/8, fill_all_rows/8]).
-on_load(init_module/0).

%% Pre-allocate {content, <<B>>, 1} tuples for all 256 bytes on module load.
%% fill_bin receives the table once per row call and uses element/2 (O(1), no alloc)
%% instead of constructing a new Content tuple per character.
init_module() ->
    T = list_to_tuple([{content, <<B>>, 1} || B <- lists:seq(0, 255)]),
    persistent_term:put(etui_ascii_content_table, T),
    ok.

%% Fixed-size array with a default value for unset indices.
%% Erlang `array` is a sparse persistent trie; get/set are O(log10 N)
%% with tiny constants, much cheaper than dict for integer-keyed dense data.

new(Size, Default) ->
    array:new(Size, [{default, Default}]).

get(Index, Arr) ->
    array:get(Index, Arr).

set(Index, Value, Arr) ->
    array:set(Index, Value, Arr).

%% Fill cells in Arr[StartIdx..MaxIdx) from a UTF-8 binary string.
%% Cell tuples are constructed directly, avoids Gleam list/fold overhead.
%%
%% Cell format mirrors buffer.gleam's Gleam types:
%%   {cell, {content, Symbol, Width}, Fg, Bg, Mod, Link}, normal cell
%%   {cell, continuation, Fg, Bg, Mod, <<>>}, wide-char trailer
fill_string(Arr, StartIdx, MaxIdx, Bin, Fg, Bg, Mod, Link) ->
    T = persistent_term:get(etui_ascii_content_table),
    fill_bin(Arr, StartIdx, MaxIdx, Bin, Fg, Bg, Mod, Link, T).

fill_bin(Arr, Idx, MaxIdx, _, _, _, _, _, _) when Idx >= MaxIdx ->
    Arr;
fill_bin(Arr, _, _, <<>>, _, _, _, _, _) ->
    Arr;
%% ASCII printable fast path: cached Content tuple, single Cell alloc per char
fill_bin(Arr, Idx, MaxIdx, <<B, Rest/binary>>, Fg, Bg, Mod, Link, T)
        when B >= 16#20, B < 16#7F ->
    Content = element(B + 1, T),
    Cell = {cell, Content, Fg, Bg, Mod, Link},
    fill_bin(array:set(Idx, Cell, Arr), Idx + 1, MaxIdx, Rest, Fg, Bg, Mod, Link, T);
%% Skip non-printable ASCII (control chars, DEL)
fill_bin(Arr, Idx, MaxIdx, <<B, Rest/binary>>, Fg, Bg, Mod, Link, T) when B < 16#20 ->
    fill_bin(Arr, Idx, MaxIdx, Rest, Fg, Bg, Mod, Link, T);
fill_bin(Arr, Idx, MaxIdx, <<16#7F, Rest/binary>>, Fg, Bg, Mod, Link, T) ->
    fill_bin(Arr, Idx, MaxIdx, Rest, Fg, Bg, Mod, Link, T);
%% Non-ASCII: grapheme cluster segmentation + East Asian width.
%% string:next_grapheme/1 returns [Codepoint|Rest] (single cp)
%% or [[Cp,...]|Rest] (ZWJ sequence / multi-cp cluster).
fill_bin(Arr, Idx, MaxIdx, Bin, Fg, Bg, Mod, Link, T) ->
    case string:next_grapheme(Bin) of
        [] -> Arr;
        [G | Rest] when is_integer(G) ->
            GBin = unicode:characters_to_binary([G]),
            W = cp_width(G),
            Cell = {cell, {content, GBin, W}, Fg, Bg, Mod, Link},
            Arr2 = array:set(Idx, Cell, Arr),
            case W >= 2 of
                true ->
                    Cont = {cell, continuation, Fg, Bg, Mod, <<>>},
                    Arr3 = case Idx + 1 < MaxIdx of
                        true  -> array:set(Idx + 1, Cont, Arr2);
                        false -> Arr2
                    end,
                    fill_bin(Arr3, Idx + 2, MaxIdx, Rest, Fg, Bg, Mod, Link, T);
                false ->
                    fill_bin(Arr2, Idx + 1, MaxIdx, Rest, Fg, Bg, Mod, Link, T)
            end;
        [[FirstCp | _] = GList | Rest] ->
            GBin = unicode:characters_to_binary(GList),
            W = cp_width(FirstCp),
            Cell = {cell, {content, GBin, W}, Fg, Bg, Mod, Link},
            Arr2 = array:set(Idx, Cell, Arr),
            case W >= 2 of
                true ->
                    Cont = {cell, continuation, Fg, Bg, Mod, <<>>},
                    Arr3 = case Idx + 1 < MaxIdx of
                        true  -> array:set(Idx + 1, Cont, Arr2);
                        false -> Arr2
                    end,
                    fill_bin(Arr3, Idx + 2, MaxIdx, Rest, Fg, Bg, Mod, Link, T);
                false ->
                    fill_bin(Arr2, Idx + 1, MaxIdx, Rest, Fg, Bg, Mod, Link, T)
            end
    end.

%% Fill an entire Width×Height buffer from scratch using array:from_list/2.
%% Each row gets the same Bin text. Builds cells as a reversed flat list,
%% reverses once at the end, then constructs the trie in one shot.
%% 3× faster than 60 sequential fill_string calls which rebuild the trie per row.
fill_all_rows(Width, Height, Bin, Fg, Bg, Mod, Link, Default) ->
    T = persistent_term:get(etui_ascii_content_table),
    RevCells = build_buffer_rev(Width, Height, 0, Bin, Fg, Bg, Mod, Link, T, Default, []),
    array:from_list(lists:reverse(RevCells), Default).

build_buffer_rev(_, Height, Row, _, _, _, _, _, _, _, RevAcc) when Row >= Height ->
    RevAcc;
build_buffer_rev(Width, Height, Row, Bin, Fg, Bg, Mod, Link, T, Default, RevAcc) ->
    RevAcc2 = build_row_rev(Width, 0, Bin, Fg, Bg, Mod, Link, T, Default, RevAcc),
    build_buffer_rev(Width, Height, Row + 1, Bin, Fg, Bg, Mod, Link, T, Default, RevAcc2).

%% Produces exactly Width cells, padding with Default if Bin is exhausted.
build_row_rev(Width, Col, _, _, _, _, _, _, _Default, RevAcc) when Col >= Width ->
    RevAcc;
build_row_rev(Width, Col, <<>>, _Fg, _Bg, _Mod, _Link, _T, Default, RevAcc) ->
    fill_rev(Width - Col, Default, RevAcc);
build_row_rev(Width, Col, <<B, Rest/binary>>, Fg, Bg, Mod, Link, T, Default, RevAcc)
        when B >= 16#20, B < 16#7F ->
    Content = element(B + 1, T),
    Cell = {cell, Content, Fg, Bg, Mod, Link},
    build_row_rev(Width, Col + 1, Rest, Fg, Bg, Mod, Link, T, Default, [Cell | RevAcc]);
build_row_rev(Width, Col, <<_B, Rest/binary>>, Fg, Bg, Mod, Link, T, Default, RevAcc) ->
    build_row_rev(Width, Col, Rest, Fg, Bg, Mod, Link, T, Default, RevAcc).

fill_rev(0, _, Acc) -> Acc;
fill_rev(N, V, Acc) -> fill_rev(N - 1, V, [V | Acc]).

%% East Asian Width, mirrors text.gleam's codepoint_cell_width/1.
cp_width(Cp) when Cp < 16#20         -> 0;
cp_width(16#7F)                       -> 0;
cp_width(Cp) when Cp >= 16#0300,
                  Cp =< 16#036F      -> 0;  % combining diacritics
cp_width(Cp) when Cp >= 16#1160,
                  Cp =< 16#11FF      -> 0;  % Hangul medial/final combining
cp_width(Cp) when Cp >= 16#FE00,
                  Cp =< 16#FE0F      -> 0;  % variation selectors
cp_width(Cp) when Cp >= 16#E0100,
                  Cp =< 16#E01EF     -> 0;  % variation selectors ext.
cp_width(16#200B)                     -> 0;
cp_width(16#200C)                     -> 0;
cp_width(16#200D)                     -> 0;
cp_width(16#FEFF)                     -> 0;
cp_width(Cp) when Cp >= 16#1100,
                  Cp =< 16#115F      -> 2;  % Hangul Jamo initial
cp_width(Cp) when Cp >= 16#2E80,
                  Cp =< 16#303E      -> 2;  % CJK Radicals / Kangxi
cp_width(Cp) when Cp >= 16#3041,
                  Cp =< 16#33FF      -> 2;  % Hiragana/Katakana/CJK compat
cp_width(Cp) when Cp >= 16#3400,
                  Cp =< 16#4DBF      -> 2;  % CJK Extension A
cp_width(Cp) when Cp >= 16#4E00,
                  Cp =< 16#9FFF      -> 2;  % CJK Unified Ideographs
cp_width(Cp) when Cp >= 16#A000,
                  Cp =< 16#A4CF      -> 2;  % Yi
cp_width(Cp) when Cp >= 16#AC00,
                  Cp =< 16#D7A3      -> 2;  % Hangul Syllables
cp_width(Cp) when Cp >= 16#F900,
                  Cp =< 16#FAFF      -> 2;  % CJK Compatibility Ideographs
cp_width(Cp) when Cp >= 16#FE30,
                  Cp =< 16#FE4F      -> 2;  % CJK Compatibility Forms
cp_width(Cp) when Cp >= 16#FF00,
                  Cp =< 16#FF60      -> 2;  % Fullwidth Forms
cp_width(Cp) when Cp >= 16#FFE0,
                  Cp =< 16#FFE6      -> 2;  % Fullwidth Signs
cp_width(Cp) when Cp >= 16#1F1E6,
                  Cp =< 16#1F1FF     -> 2;  % Regional Indicators (flags)
cp_width(Cp) when Cp >= 16#1F300,
                  Cp =< 16#1FAFF     -> 2;  % Emoji (misc/pictographs/etc.)
cp_width(Cp) when Cp >= 16#20000,
                  Cp =< 16#2FFFD     -> 2;  % CJK Extensions B–F
cp_width(Cp) when Cp >= 16#30000,
                  Cp =< 16#3FFFD     -> 2;  % CJK Extension G+
cp_width(_)                           -> 1.