src/qrkit.erl

-module(qrkit).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/qrkit.gleam").
-export([package_version/0, new/1, with_ecc/2, with_exact_version/2, with_min_version/2, with_eci/2, with_symbol/2, with_mode_preference/2, build/1, encode/1, encode_split_with/3, encode_split/2, version/1, size/1, width/1, height/1, symbol_size/1, error_correction/1, error_correction_designator/1, symbol/1, mask/1, rows/1, module_at/3]).
-export_type([builder/0, qr_code/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(" Public API for the qrkit package.\n").

-opaque builder() :: {builder,
        binary(),
        qrkit@types:error_correction(),
        gleam@option:option(integer()),
        gleam@option:option(integer()),
        qrkit@types:symbol(),
        qrkit@types:mode_preference()}.

-opaque qr_code() :: {qr_code,
        integer(),
        integer(),
        integer(),
        qrkit@types:error_correction(),
        qrkit@types:symbol(),
        integer(),
        list(list(boolean()))}.

-file("src/qrkit.gleam", 67).
?DOC(" The package version.\n").
-spec package_version() -> binary().
package_version() ->
    <<"0.1.0"/utf8>>.

-file("src/qrkit.gleam", 72).
?DOC(" Create a new builder from input text.\n").
-spec new(binary()) -> builder().
new(Data) ->
    {builder, Data, medium, none, none, standard, auto}.

-file("src/qrkit.gleam", 82).
?DOC(" Set the desired error correction level.\n").
-spec with_ecc(builder(), qrkit@types:error_correction()) -> builder().
with_ecc(Builder, Ecc) ->
    {builder, Data, _, Min_version, Eci, Symbol, Preference} = Builder,
    {builder, Data, Ecc, Min_version, Eci, Symbol, Preference}.

-file("src/qrkit.gleam", 98).
?DOC(
    " Pin the symbol version exactly.\n"
    "\n"
    " The value is interpreted relative to the active symbol family: Standard QR\n"
    " accepts 1..40, Micro QR accepts 1..4 (M1..M4), and rMQR accepts 1..32\n"
    " (R7x43..R17x139).\n"
    "\n"
    " If the payload, mode, or ECC level cannot be satisfied at the requested\n"
    " version, `build` returns `Error(DataExceedsCapacity)` or\n"
    " `Error(IncompatibleOptions)` instead of bumping to a larger version.\n"
    " When no exact version is configured, the encoder selects the smallest\n"
    " version that fits the payload.\n"
).
-spec with_exact_version(builder(), integer()) -> builder().
with_exact_version(Builder, Version) ->
    {builder, Data, Ecc, _, Eci, Symbol, Preference} = Builder,
    {builder, Data, Ecc, {some, Version}, Eci, Symbol, Preference}.

-file("src/qrkit.gleam", 107).
?DOC(
    " Compatibility alias for [`with_exact_version`](#with_exact_version).\n"
    "\n"
    " Despite the historical name, this pins the symbol version exactly rather\n"
    " than setting a lower bound. New code should prefer `with_exact_version`.\n"
).
-spec with_min_version(builder(), integer()) -> builder().
with_min_version(Builder, Min_version) ->
    with_exact_version(Builder, Min_version).

-file("src/qrkit.gleam", 116).
?DOC(
    " Add an optional ECI assignment designator before the data segments.\n"
    "\n"
    " ECI is only supported for Standard QR. Valid designators are in the range\n"
    " 0..999999; invalid values surface as `Error(InvalidEciDesignator(..))`\n"
    " during `build`.\n"
).
-spec with_eci(builder(), integer()) -> builder().
with_eci(Builder, Designator) ->
    {builder, Data, Ecc, Min_version, _, Symbol, Preference} = Builder,
    {builder, Data, Ecc, Min_version, {some, Designator}, Symbol, Preference}.

-file("src/qrkit.gleam", 122).
?DOC(" Select the symbol family.\n").
-spec with_symbol(builder(), qrkit@types:symbol()) -> builder().
with_symbol(Builder, Symbol) ->
    {builder, Data, Ecc, Min_version, Eci, _, Preference} = Builder,
    {builder, Data, Ecc, Min_version, Eci, Symbol, Preference}.

-file("src/qrkit.gleam", 128).
?DOC(" Change the mode optimisation strategy.\n").
-spec with_mode_preference(builder(), qrkit@types:mode_preference()) -> builder().
with_mode_preference(Builder, Preference) ->
    {builder, Data, Ecc, Min_version, Eci, Symbol, _} = Builder,
    {builder, Data, Ecc, Min_version, Eci, Symbol, Preference}.

-file("src/qrkit.gleam", 204).
-spec validate_min_version(gleam@option:option(integer()), qrkit@types:symbol()) -> {ok,
        nil} |
    {error, qrkit@error:encode_error()}.
validate_min_version(Min_version, Symbol) ->
    case Min_version of
        none ->
            {ok, nil};

        {some, Value} ->
            Upper = case Symbol of
                standard ->
                    40;

                micro ->
                    4;

                rectangular ->
                    32
            end,
            case (Value < 1) orelse (Value > Upper) of
                true ->
                    {error, {invalid_version, Value}};

                false ->
                    {ok, nil}
            end
    end.

-file("src/qrkit.gleam", 224).
-spec validate_eci(gleam@option:option(integer()), qrkit@types:symbol()) -> {ok,
        nil} |
    {error, qrkit@error:encode_error()}.
validate_eci(Eci, Symbol) ->
    case Eci of
        none ->
            {ok, nil};

        {some, Designator} ->
            case (Designator < 0) orelse (Designator > 999999) of
                true ->
                    {error, {invalid_eci_designator, Designator}};

                false ->
                    case Symbol =:= standard of
                        true ->
                            {ok, nil};

                        false ->
                            {error,
                                {incompatible_options,
                                    <<"ECI is only supported for Standard QR"/utf8>>}}
                    end
            end
    end.

-file("src/qrkit.gleam", 193).
-spec validate_builder_options(
    gleam@option:option(integer()),
    gleam@option:option(integer()),
    qrkit@types:symbol()
) -> {ok, nil} | {error, qrkit@error:encode_error()}.
validate_builder_options(Min_version, Eci, Symbol) ->
    case validate_min_version(Min_version, Symbol) of
        {error, Error} ->
            {error, Error};

        {ok, nil} ->
            validate_eci(Eci, Symbol)
    end.

-file("src/qrkit.gleam", 137).
?DOC(" Build a QR code from the accumulated builder configuration.\n").
-spec build(builder()) -> {ok, qr_code()} | {error, qrkit@error:encode_error()}.
build(Builder) ->
    {builder, Data, Ecc, Min_version, Eci, Symbol, Preference} = Builder,
    case Data =:= <<""/utf8>> of
        true ->
            {error, empty_input};

        false ->
            case validate_builder_options(Min_version, Eci, Symbol) of
                {error, Error} ->
                    {error, Error};

                {ok, nil} ->
                    case Symbol of
                        standard ->
                            case qrkit@internal@standard:encode(
                                Data,
                                Ecc,
                                Min_version,
                                Eci,
                                Preference
                            ) of
                                {ok, Encoded} ->
                                    {ok,
                                        {qr_code,
                                            qrkit@internal@standard:version(
                                                Encoded
                                            ),
                                            qrkit@internal@standard:width(
                                                Encoded
                                            ),
                                            qrkit@internal@standard:height(
                                                Encoded
                                            ),
                                            Ecc,
                                            Symbol,
                                            qrkit@internal@standard:mask(
                                                Encoded
                                            ),
                                            qrkit@internal@standard:rows(
                                                Encoded
                                            )}};

                                {error, Encode_error} ->
                                    {error, Encode_error}
                            end;

                        micro ->
                            case qrkit@internal@micro:encode(
                                Data,
                                Ecc,
                                Min_version,
                                Preference
                            ) of
                                {ok, Encoded@1} ->
                                    {ok,
                                        {qr_code,
                                            qrkit@internal@micro:version(
                                                Encoded@1
                                            ),
                                            qrkit@internal@micro:width(
                                                Encoded@1
                                            ),
                                            qrkit@internal@micro:height(
                                                Encoded@1
                                            ),
                                            Ecc,
                                            Symbol,
                                            qrkit@internal@micro:mask(Encoded@1),
                                            qrkit@internal@micro:rows(Encoded@1)}};

                                {error, Encode_error@1} ->
                                    {error, Encode_error@1}
                            end;

                        rectangular ->
                            case qrkit@internal@rmqr:encode(
                                Data,
                                Ecc,
                                Min_version,
                                Preference
                            ) of
                                {ok, Encoded@2} ->
                                    {ok,
                                        {qr_code,
                                            qrkit@internal@rmqr:version(
                                                Encoded@2
                                            ),
                                            qrkit@internal@rmqr:width(Encoded@2),
                                            qrkit@internal@rmqr:height(
                                                Encoded@2
                                            ),
                                            Ecc,
                                            Symbol,
                                            qrkit@internal@rmqr:mask(Encoded@2),
                                            qrkit@internal@rmqr:rows(Encoded@2)}};

                                {error, Encode_error@2} ->
                                    {error, Encode_error@2}
                            end
                    end
            end
    end.

-file("src/qrkit.gleam", 77).
?DOC(" Encode input text using the default builder configuration.\n").
-spec encode(binary()) -> {ok, qr_code()} | {error, qrkit@error:encode_error()}.
encode(Data) ->
    _pipe = new(Data),
    build(_pipe).

-file("src/qrkit.gleam", 257).
?DOC(" Same as `encode_split` but with a caller-chosen error correction level.\n").
-spec encode_split_with(binary(), integer(), qrkit@types:error_correction()) -> {ok,
        list(qr_code())} |
    {error, qrkit@error:encode_error()}.
encode_split_with(Data, Max_version, Ecc) ->
    case qrkit@internal@structured_append:encode(Data, Max_version, Ecc) of
        {error, Error} ->
            {error, Error};

        {ok, Encodes} ->
            {ok,
                gleam@list:map(
                    Encodes,
                    fun(Encoded) ->
                        {qr_code,
                            qrkit@internal@standard:version(Encoded),
                            qrkit@internal@standard:width(Encoded),
                            qrkit@internal@standard:height(Encoded),
                            Ecc,
                            standard,
                            qrkit@internal@standard:mask(Encoded),
                            qrkit@internal@standard:rows(Encoded)}
                    end
                )}
    end.

-file("src/qrkit.gleam", 249).
?DOC(
    " Split data into multiple symbols using Structured Append (ISO/IEC 18004 §8.2).\n"
    "\n"
    " Each returned QR carries the 20-bit Structured Append header so a compliant\n"
    " reader can reassemble the original message. When `data` fits in a single QR\n"
    " at `max_version`, the returned list contains exactly one symbol with no SA\n"
    " header. Uses the Medium error correction level — call `encode_split_with`\n"
    " for a different level.\n"
).
-spec encode_split(binary(), integer()) -> {ok, list(qr_code())} |
    {error, qrkit@error:encode_error()}.
encode_split(Data, Max_version) ->
    encode_split_with(Data, Max_version, medium).

-file("src/qrkit.gleam", 283).
?DOC(
    " Return the symbol version number. Standard QR returns 1..40, Micro QR 1..4\n"
    " (M1..M4), and rMQR 1..32 (R7x43..R17x139).\n"
).
-spec version(qr_code()) -> integer().
version(Qr) ->
    {qr_code, Version, _, _, _, _, _, _} = Qr,
    Version.

-file("src/qrkit.gleam", 290).
?DOC(
    " Return the symbol side length in modules. For non-square rMQR symbols this\n"
    " is the width; use `width` and `height` for the explicit dimensions.\n"
).
-spec size(qr_code()) -> integer().
size(Qr) ->
    {qr_code, _, Width, _, _, _, _, _} = Qr,
    Width.

-file("src/qrkit.gleam", 296).
?DOC(" Return the symbol width in modules.\n").
-spec width(qr_code()) -> integer().
width(Qr) ->
    {qr_code, _, Width, _, _, _, _, _} = Qr,
    Width.

-file("src/qrkit.gleam", 302).
?DOC(" Return the symbol height in modules.\n").
-spec height(qr_code()) -> integer().
height(Qr) ->
    {qr_code, _, _, Height, _, _, _, _} = Qr,
    Height.

-file("src/qrkit.gleam", 308).
?DOC(" Return the symbol dimensions in modules.\n").
-spec symbol_size(qr_code()) -> {integer(), integer()}.
symbol_size(Qr) ->
    {width(Qr), height(Qr)}.

-file("src/qrkit.gleam", 313).
?DOC(" Return the error correction level used by this symbol.\n").
-spec error_correction(qr_code()) -> qrkit@types:error_correction().
error_correction(Qr) ->
    {qr_code, _, _, _, Ecc, _, _, _} = Qr,
    Ecc.

-file("src/qrkit.gleam", 319).
?DOC(" Return the canonical single-letter ECC designator.\n").
-spec error_correction_designator(qrkit@types:error_correction()) -> binary().
error_correction_designator(Ecc) ->
    case Ecc of
        low ->
            <<"L"/utf8>>;

        medium ->
            <<"M"/utf8>>;

        quartile ->
            <<"Q"/utf8>>;

        high ->
            <<"H"/utf8>>
    end.

-file("src/qrkit.gleam", 329).
?DOC(" Return the symbol family used by this QR code.\n").
-spec symbol(qr_code()) -> qrkit@types:symbol().
symbol(Qr) ->
    {qr_code, _, _, _, _, Symbol, _, _} = Qr,
    Symbol.

-file("src/qrkit.gleam", 336).
?DOC(
    " Return the mask pattern that was applied. Standard QR returns 0..7,\n"
    " Micro QR 0..3, and rMQR always 4 (rMQR uses a single fixed mask).\n"
).
-spec mask(qr_code()) -> integer().
mask(Qr) ->
    {qr_code, _, _, _, _, _, Mask, _} = Qr,
    Mask.

-file("src/qrkit.gleam", 357).
?DOC(" Return the symbol matrix as rows of booleans.\n").
-spec rows(qr_code()) -> list(list(boolean())).
rows(Qr) ->
    {qr_code, _, _, _, _, _, _, Rows} = Qr,
    Rows.

-file("src/qrkit.gleam", 345).
?DOC(
    " Return a single module from the symbol matrix.\n"
    "\n"
    " Returns `Error(ModuleOutOfBounds(..))` when `x` or `y` fall outside the\n"
    " matrix dimensions.\n"
).
-spec module_at(qr_code(), integer(), integer()) -> {ok, boolean()} |
    {error, qrkit@error:matrix_access_error()}.
module_at(Qr, X, Y) ->
    case begin
        _pipe = rows(Qr),
        qrkit@internal@util:at(_pipe, Y)
    end of
        {ok, Row} ->
            case qrkit@internal@util:at(Row, X) of
                {ok, Value} ->
                    {ok, Value};

                {error, _} ->
                    {error, {module_out_of_bounds, X, Y, width(Qr), height(Qr)}}
            end;

        {error, _} ->
            {error, {module_out_of_bounds, X, Y, width(Qr), height(Qr)}}
    end.