% \iffalse meta-comment
% Copyright (C) 2026 Andreas Willibald Weber
%
% This work may be distributed and/or modified under the conditions of the
% LaTeX Project Public License, either version 1.3c of this license or (at your
% option) any later version. The latest version of the license is available at
% https://www.latex-project.org/lppl.txt.
%
% This work has the LPPL maintenance status `maintained'. The Current
% Maintainer of this work is Andreas Willibald Weber.
%
% This file is part of the `kanban-board' package. The complete list of files
% constituting the work appears in the main source file kanban-board.dtx.
% \fi
%
%    \begin{macrocode}
%<*package>
%    \end{macrocode}
%
% \subsection{Palette and column registries}
%
% Palette names map to lists of heading backgrounds and, for starred
% definitions, to paired lists of heading foregrounds. Both maps are global
% property lists. Column names are normalized separately and receive
% board-local numeric identifiers, so user text never becomes part of an
% internal control-sequence name. The registry therefore has two
% responsibilities. It maintains the persistent palette catalogue, and it
% converts the textual column interface into the compact board-local
% representation that the renderers consume.
%
% \paragraph{Plain palette definitions.}
% An unstarred definition accepts a name and a non-empty comma list of
% heading backgrounds. A successful definition replaces any palette of the
% same name and removes a previously stored foreground list. If the redefined
% name is the selected palette, the persistent snapshot is refreshed
% immediately so that later boards use the new colors without a second
% selection. An invalid definition leaves every registry unchanged.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_palette_define:nn #1#2
  {
    \tl_set:Nn \l__kanban_palette_name_tl {#1}
    \tl_trim_spaces:N \l__kanban_palette_name_tl
    \tl_if_blank:VTF \l__kanban_palette_name_tl
      {\msg_warning:nn {kanban-board}{empty-palette-name}}
      {
        \clist_set:Nn \l__kanban_palette_colors_clist {#2}
        \clist_if_empty:NTF \l__kanban_palette_colors_clist
          {\use:x {\msg_warning:nnnn {kanban-board}{empty-palette}
            {\exp_not:V \l__kanban_palette_name_tl}{colors}}}
          {
            \prop_gput:NVV \g__kanban_palettes_prop
              \l__kanban_palette_name_tl \l__kanban_palette_colors_clist
            \prop_gremove:NV \g__kanban_palette_texts_prop
              \l__kanban_palette_name_tl
            \tl_if_eq:NNT \l__kanban_palette_name_tl
              \g__kanban_palette_name_tl
              {
                \clist_gset_eq:NN \g__kanban_palette_clist
                  \l__kanban_palette_colors_clist
                \clist_gclear:N \g__kanban_palette_fg_clist
              }
          }
      }
  }
%    \end{macrocode}
%
% \paragraph{Paired palette definitions.}
% The starred syntax associates every heading background with an explicit
% heading foreground. Each comma item must contain exactly one |/| separator
% and two non-empty components. The item helper validates one pair and
% appends its components to the working lists, or records the failure. The
% definition is committed only after every pair has passed. A single
% malformed item rejects the whole palette rather than installing a partial
% result.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_palette_pair:n #1
  {
    \seq_set_split:Nnn \l__kanban_palette_pair_seq {/} {#1}
    \int_compare:nNnTF {\seq_count:N \l__kanban_palette_pair_seq} = {2}
      {
        \tl_set:Nx \l__kanban_pair_bg_tl
          {\seq_item:Nn \l__kanban_palette_pair_seq {1}}
        \tl_set:Nx \l__kanban_pair_fg_tl
          {\seq_item:Nn \l__kanban_palette_pair_seq {2}}
        \tl_trim_spaces:N \l__kanban_pair_bg_tl
        \tl_trim_spaces:N \l__kanban_pair_fg_tl
        \bool_if:nTF
          {
            \tl_if_blank_p:V \l__kanban_pair_bg_tl
            || \tl_if_blank_p:V \l__kanban_pair_fg_tl
          }
          {\bool_set_true:N \l__kanban_bad_palette_bool}
          {
            \clist_put_right:NV \l__kanban_palette_bg_clist
              \l__kanban_pair_bg_tl
            \clist_put_right:NV \l__kanban_palette_fg_clist
              \l__kanban_pair_fg_tl
          }
      }
      {\bool_set_true:N \l__kanban_bad_palette_bool}
  }
\cs_new_protected:Npn \__kanban_palette_define_pairs:nn #1#2
  {
    \tl_set:Nn \l__kanban_palette_name_tl {#1}
    \tl_trim_spaces:N \l__kanban_palette_name_tl
    \clist_clear:N \l__kanban_palette_bg_clist
    \clist_clear:N \l__kanban_palette_fg_clist
    \bool_set_false:N \l__kanban_bad_palette_bool
    \tl_if_blank:VTF \l__kanban_palette_name_tl
      {\msg_warning:nn {kanban-board}{empty-palette-name}}
      {
        \clist_set:Nn \l__kanban_palette_colors_clist {#2}
        \clist_if_empty:NTF \l__kanban_palette_colors_clist
          {\use:x {\msg_warning:nnnn {kanban-board}{empty-palette}
            {\exp_not:V \l__kanban_palette_name_tl}{color/text~pairs}}}
          {
            \clist_map_function:NN \l__kanban_palette_colors_clist
              \__kanban_palette_pair:n
            \bool_if:NTF \l__kanban_bad_palette_bool
              {\use:x {\msg_warning:nnn {kanban-board}{malformed-palette-pair}
                {\exp_not:V \l__kanban_palette_name_tl}}}
              {
                \prop_gput:NVV \g__kanban_palettes_prop
                  \l__kanban_palette_name_tl
                  \l__kanban_palette_bg_clist
                \prop_gput:NVV \g__kanban_palette_texts_prop
                  \l__kanban_palette_name_tl
                  \l__kanban_palette_fg_clist
                \tl_if_eq:NNT \l__kanban_palette_name_tl
                  \g__kanban_palette_name_tl
                  {
                    \clist_gset_eq:NN \g__kanban_palette_clist
                      \l__kanban_palette_bg_clist
                    \clist_gset_eq:NN \g__kanban_palette_fg_clist
                      \l__kanban_palette_fg_clist
                  }
              }
          }
      }
  }
%    \end{macrocode}
%
% \paragraph{Persistent selection and board-local selection.}
% Selecting a palette is distinct from defining one. Document-wide setup
% replaces the persistent defaults used by future boards. A board option
% replaces only the snapshot of the current board. Both selectors verify the
% requested name against the same global registry and keep the current
% selection when the name is unknown. The board initializer takes the initial
% local snapshot when a board begins.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_palette_select_global:n #1
  {
    \tl_set:Nn \l__kanban_palette_name_tl {#1}
    \tl_trim_spaces:N \l__kanban_palette_name_tl
    \prop_get:NVNTF \g__kanban_palettes_prop
      \l__kanban_palette_name_tl \l__kanban_palette_bg_tl
      {
        \clist_gset:NV \g__kanban_palette_clist \l__kanban_palette_bg_tl
        \prop_get:NVNTF \g__kanban_palette_texts_prop
          \l__kanban_palette_name_tl \l__kanban_palette_fg_tl
          {\clist_gset:NV \g__kanban_palette_fg_clist
            \l__kanban_palette_fg_tl}
          {\clist_gclear:N \g__kanban_palette_fg_clist}
        \tl_gset_eq:NN \g__kanban_palette_name_tl
          \l__kanban_palette_name_tl
      }
      {\use:x {\msg_warning:nnnn {kanban-board}{unknown-palette}
        {palette}{\exp_not:V \l__kanban_palette_name_tl}}}
  }
\cs_new_protected:Npn \__kanban_palette_select_board:n #1
  {
    \tl_set:Nn \l__kanban_palette_name_tl {#1}
    \tl_trim_spaces:N \l__kanban_palette_name_tl
    \prop_get:NVNTF \g__kanban_palettes_prop
      \l__kanban_palette_name_tl \l__kanban_palette_bg_tl
      {
        \clist_set:NV \l__kanban_board_palette_clist
          \l__kanban_palette_bg_tl
        \prop_get:NVNTF \g__kanban_palette_texts_prop
          \l__kanban_palette_name_tl \l__kanban_palette_fg_tl
          {\clist_set:NV \l__kanban_board_palette_fg_clist
            \l__kanban_palette_fg_tl}
          {\clist_clear:N \l__kanban_board_palette_fg_clist}
      }
      {\use:x {\msg_warning:nnnn {kanban-board}{unknown-palette}
        {board~palette}{\exp_not:V \l__kanban_palette_name_tl}}}
  }
\cs_new_protected:Npn \kanban@paletteboardinit
  {
    \clist_set_eq:NN \l__kanban_board_palette_clist
      \g__kanban_palette_clist
    \clist_set_eq:NN \l__kanban_board_palette_fg_clist
      \g__kanban_palette_fg_clist
  }
%    \end{macrocode}
%
% \paragraph{Cyclic palette lookup.}
% A board may contain more columns than its palette contains colors. The
% lookup therefore maps each one-based column number cyclically onto the
% board palette and publishes both the background and its heading foreground
% through two LaTeX2e-facing macros. If the selected palette has no explicit
% foreground list, the heading color of the board theme is used.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@palettecolor #1
  {
    \int_set:Nn \l__kanban_palette_count_int
      {\clist_count:N \l__kanban_board_palette_clist}
    \int_set:Nn \l__kanban_palette_index_int
      {1 + \int_mod:nn {#1 - 1} {\l__kanban_palette_count_int}}
    \tl_set:Nx \l__kanban_palette_item_tl
      {
        \clist_item:Nn \l__kanban_board_palette_clist
          {\l__kanban_palette_index_int}
      }
    \cs_set:Npx \kb@thiscolor {\tl_use:N \l__kanban_palette_item_tl}
    \clist_if_empty:NTF \l__kanban_board_palette_fg_clist
      {\cs_set:Npn \kb@thisheadingtext {\kanban@headingtext}}
      {
        \tl_set:Nx \l__kanban_palette_item_tl
          {
            \clist_item:Nn \l__kanban_board_palette_fg_clist
              {\l__kanban_palette_index_int}
          }
        \cs_set:Npx \kb@thisheadingtext
          {\tl_use:N \l__kanban_palette_item_tl}
      }
  }
%    \end{macrocode}
%
% \begin{macro}{\kanbandefinepalette}
% \begin{macro}{\kbdefinepalette}
% The public definition command exposes both registry formats through one
% interface. The unstarred form accepts a list of backgrounds and the starred
% form accepts background/foreground pairs. The long command is the stable
% public name. The concise alias is installed only when that name is free, so
% loading \textsf{kanban-board} never overwrites a command owned by another
% package.
%    \begin{macrocode}
\NewDocumentCommand \kanbandefinepalette {smm}
  {
    \IfBooleanTF{#1}
      {\__kanban_palette_define_pairs:nn {#2}{#3}}
      {\__kanban_palette_define:nn {#2}{#3}}
  }
\cs_if_exist:NTF \kbdefinepalette
  {\msg_warning:nnnn {kanban-board}{alias-taken}
    {kbdefinepalette}{kanbandefinepalette}}
  {\cs_new_eq:NN \kbdefinepalette \kanbandefinepalette}
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \paragraph{Internal selection bridges.}
% The setup and board-key layers are written partly in LaTeX2e syntax. These
% wrappers give those layers stable names for the selection functions and
% add no state or validation of their own.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@selectpalette #1
  {\__kanban_palette_select_global:n {#1}}
\cs_new_protected:Npn \kanban@selectboardpalette #1
  {\__kanban_palette_select_board:n {#1}}
%    \end{macrocode}
%
% \paragraph{Column-name normalization.}
% Column identity is case-insensitive and ignores formatting, but the
% visible heading keeps the spelling given in the column list. The
% single-name function therefore produces two results. It stores the trimmed
% input as the display text and writes the lookup key into the destination
% variable. The key is the purified text of the name in uppercase. Ordinary
% macros are expanded, and formatting commands and braces are removed. A
% column declared as |\emph{Soon}| is therefore addressed as |soon| by a
% task, and the heading still shows the emphasized text. Purification also
% keeps robust commands out of the expanded definitions that later publish
% the key list. The list function applies that rule to every item, ignores
% blank items, records the first spelling of each key in a display map, and
% diagnoses duplicates after normalization. Membership is tested in the
% display map rather than in the key list, because a property list treats
% every key as a string, whereas the comma list test does not support items
% that contain a brace group. A duplicate is a user error rather than a
% recoverable preference, so it produces an error with corrective help, and
% the first occurrence is retained.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_column_normalize:nN #1#2
  {
    \tl_set:Nn \l__kanban_column_display_tl {#1}
    \tl_trim_spaces:N \l__kanban_column_display_tl
    \tl_set:Nx #2
      {\exp_args:NV \text_purify:n \l__kanban_column_display_tl}
    \tl_set:Nx #2 {\exp_args:NV \text_uppercase:n #2}
    \tl_trim_spaces:N #2
  }
\cs_new_protected:Npn \__kanban_columns_normalize:nNN #1#2#3
  {
    \clist_clear:N #2
    \prop_clear:N #3
    \clist_map_inline:nn {#1}
      {
        \__kanban_column_normalize:nN {##1} \l__kanban_column_tl
        \tl_if_blank:VF \l__kanban_column_tl
          {
            \prop_if_in:NVTF #3 \l__kanban_column_tl
              {\PackageError{kanban-board}
                {duplicate~kanban~column~
                  '\tl_use:N \l__kanban_column_tl'}
                {Column~names~are~case-insensitive.\MessageBreak
                  Remove~the~repeated~name~from~the~column~list.}}
              {
                \clist_put_right:NV #2 \l__kanban_column_tl
                \prop_put:NVV #3 \l__kanban_column_tl
                  \l__kanban_column_display_tl
              }
          }
      }
  }
%    \end{macrocode}
%
% \paragraph{Persistent and board-local column lists.}
% As with palettes, columns have a persistent default and a board-local
% snapshot. Each consists of the normalized key list and the display map.
% Document setup accepts only a non-empty normalized list and makes both
% parts global. Board initialization copies the defaults locally. A non-blank
% board override replaces the copy without affecting later boards.
%    \begin{macrocode}
\cs_new_protected:Npn \__kanban_columns_select_global:n #1
  {
    \__kanban_columns_normalize:nNN {#1}
      \l__kanban_columns_tmp_clist
      \l__kanban_column_display_tmp_prop
    \clist_if_empty:NTF \l__kanban_columns_tmp_clist
      {\msg_warning:nn {kanban-board}{empty-column-list}}
      {
        \clist_gset_eq:NN \g__kanban_columns_clist
          \l__kanban_columns_tmp_clist
        \prop_gset_eq:NN \g__kanban_column_display_prop
          \l__kanban_column_display_tmp_prop
      }
  }
\cs_new_protected:Npn \kanban@selectcolumns #1
  {\__kanban_columns_select_global:n {#1}}
\cs_new_protected:Npn \kanban@columnsboardinit
  {
    \clist_set_eq:NN \l__kanban_board_columns_clist
      \g__kanban_columns_clist
    \prop_set_eq:NN \l__kanban_board_column_display_prop
      \g__kanban_column_display_prop
  }
\cs_new_protected:Npn \kanban@selectboardcolumns #1
  {
    \tl_if_blank:nF {#1}
      {
        \__kanban_columns_normalize:nNN {#1}
          \l__kanban_columns_tmp_clist
          \l__kanban_column_display_tmp_prop
        \clist_if_empty:NF \l__kanban_columns_tmp_clist
          {
            \clist_set_eq:NN \l__kanban_board_columns_clist
              \l__kanban_columns_tmp_clist
            \prop_set_eq:NN \l__kanban_board_column_display_prop
              \l__kanban_column_display_tmp_prop
          }
      }
  }
%    \end{macrocode}
%
% \paragraph{Task-target resolution.}
% A task names a column textually, but collection must append the task to a
% numeric board-local list. The resolver normalizes the supplied name,
% exposes the normalized text for diagnostics, and looks it up in the
% name-to-identifier map of the active board. A missing key leaves the
% identifier empty, which lets the collection layer issue the
% context-specific error.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@normtaskcol #1
  {
    \__kanban_column_normalize:nN {#1} \l__kanban_column_tl
    \cs_set:Npx \kbtask@column {\tl_use:N \l__kanban_column_tl}
    \cs_set:Npn \kbtask@columnid { }
    \prop_get:NVNTF
      \l__kanban_column_ids_prop \l__kanban_column_tl
      \l__kanban_column_id_tl
      {\cs_set:Npx \kbtask@columnid
        {\tl_use:N \l__kanban_column_id_tl}}
      { }
  }
%    \end{macrocode}
%
% \paragraph{Numeric column registration.}
% Before tasks can be collected, every normalized board column receives a
% consecutive identifier. The setup function also prepares the printable list
% of valid keys for error messages and delegates the creation of per-column
% counters, lists, colors, and renderer entries to |\kanban@setupcolumn|,
% which is defined in the board part. The printable list is expanded through
% |\text_expand:n|, which expands the comma list but leaves any robust
% command intact, so the expanded definition cannot be derailed. The display
% text is passed by value so that it is stored unexpanded for the heading.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@setupboardcolumns
  {
    \cs_set:Npx \kanban@availablecolumns
      {
        \text_expand:n
          {\clist_use:Nn \l__kanban_board_columns_clist {,}}
      }
    \prop_clear:N \l__kanban_column_ids_prop
    \int_zero:N \l__kanban_column_index_int
    \clist_map_inline:Nn \l__kanban_board_columns_clist
      {
        \int_incr:N \l__kanban_column_index_int
        \tl_set:Nx \l__kanban_column_id_tl
          {\int_use:N \l__kanban_column_index_int}
        \prop_put:NnV \l__kanban_column_ids_prop {##1}
          \l__kanban_column_id_tl
        \prop_get:NnN \l__kanban_board_column_display_prop {##1}
          \l__kanban_column_display_tl
        \exp_args:NVV \kanban@setupcolumn
          \l__kanban_column_display_tl \l__kanban_column_id_tl
      }
  }
%    \end{macrocode}
%
% \paragraph{Choice normalization and registry cleanup.}
% Several key values, including modes and date-format names, share the same
% case-insensitive choice syntax. The choice helper trims and lowercases such
% a value before publishing it as |\kb@choice|. The lowercase conversion
% expands ordinary macros and leaves robust commands intact, and the
% published copy is not expanded a second time. A robust command in a choice
% is therefore reported as an unknown value instead of derailing the
% definition. At the end of the board
% lifecycle, the cleanup function removes all dynamically created per-column
% control sequences so that completed boards do not retain card material or
% counters in the hash table. The names are constructed from character data,
% so the |c| argument type is required.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@normalizechoice #1
  {
    \tl_set:Nn \l__kanban_choice_tl {#1}
    \tl_trim_spaces:N \l__kanban_choice_tl
    \tl_set:Nx \l__kanban_choice_tl
      {\exp_args:NV \text_lowercase:n \l__kanban_choice_tl}
    \cs_set:Npx \kb@choice {\exp_not:V \l__kanban_choice_tl}
  }
\cs_new_protected:Npn \kanban@cleanupboard
  {
    \int_step_inline:nn {\kanban@ncols}
      {
        \cs_undefine:c {kanban@name@##1}
        \cs_undefine:c {kanban@color@##1}
        \cs_undefine:c {kanban@headingtext@##1}
        \cs_undefine:c
          {kanban@list@\kanban@boardgroup @##1}
        \cs_undefine:c
          {kanban@count@\kanban@boardgroup @##1}
      }
  }
%    \end{macrocode}
%
%    \begin{macrocode}
%</package>
%    \end{macrocode}
