% \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{Task collection}
%
% The task key family stores metadata without expanding the card text. The
% key name of an unknown option is passed by expansion, because
% \textsf{l3msg} shows message arguments without expanding them. A
% valid declaration appends one protected renderer call to the selected
% board-local list. Invalid or missing targets are diagnosed and never affect
% the counter. Collection and rendering are separate phases. This permits the
% environment to determine the tallest heading, the highest column, and the
% final column counts before any task card is typeset.
%
% \paragraph{Task-option keys.}
% The task family translates user-facing metadata into temporary task
% macros. Priority and background use validated setters. Author and deadline
% keep their token lists. The |due| key aliases |deadline| and |background|
% aliases |bg|. The |late| and |completed| keys are valueless flags, so a
% misleading form such as |completed=false| is rejected. An unknown task
% option is ignored with a scoped warning.
%    \begin{macrocode}
\ExplSyntaxOn
\keys_define:nn {kanban/task}
  {
    prio .code:n = {\kanban@setpriority{#1}},
    author .code:n = {\def\kbtask@author{#1}},
    deadline .code:n = {\def\kbtask@deadline{#1}},
    due .meta:n = {deadline={#1}},
    late .code:n = {\def\kbtask@late{1}},
    late .value_forbidden:n = true,
    completed .code:n = {\def\kbtask@completed{1}},
    completed .value_forbidden:n = true,
    bg .code:n = {\kanban@setbackground{#1}},
    background .meta:n = {bg={#1}},
    unknown .code:n =
      {\use:x {\msg_warning:nnnn {kanban-board}{unknown-key}
        {task}{\l_keys_key_str}}}
  }
\cs_new_protected:Npn \kanban@processtaskoptions #1
  {\keys_set:nn {kanban/task}{#1}}
\ExplSyntaxOff
%    \end{macrocode}
%
% \paragraph{Validation and deferred task collection.}
% The collector first gives every metadata field an empty value and
% processes the option list. It then requires an active board, a non-empty
% normalized target, and a target present in the identifier map of the
% board. Each failed condition emits a specific error and ignores the task.
% A task inside a rejected nested board is discarded silently because the
% nesting error has already explained the cause. A task outside any board is
% diagnosed on its own.
%
% For a valid target, the counter is incremented globally and a
% self-contained eight-argument call to |\kanban@rendercard| is appended
% globally to the task list of the column. The renderer command is kept
% unexpanded for the rendering phase, the column color is resolved at once
% because it is already fixed, and the task body and all metadata are
% protected from expansion. The deadline is then checked once, so that any
% warning names the line of the declaration.
%    \begin{macrocode}
\newcommand\kanban@dotask[3][]{%
  \def\kbtask@prio{}\def\kbtask@author{}\def\kbtask@deadline{}%
  \def\kbtask@late{}\def\kbtask@completed{}\def\kbtask@bg{}%
  \kanban@processtaskoptions{#1}%
  \ifbool{kanban@active}
    {\kanban@normtaskcol{#2}%
     \ifdefempty\kbtask@column
       {\PackageError{kanban-board}
         {task has no assigned column.
           \MessageBreak Valid columns: \kanban@availablecolumns}
         {The first mandatory argument of
           \string\kanbantask\space must name a column from the
           enclosing board.\MessageBreak The task was ignored.}}
       {\ifdefempty\kbtask@columnid
       {\PackageError{kanban-board}
         {unknown kanban column '#2'.
           \MessageBreak Valid columns: \kanban@availablecolumns}
         {Column names are matched without regard to letter case.
           \MessageBreak
           The task was ignored because its normalized target is not in
           this board.}}
       {\expandafter\numgdef
           \csname kanban@count@\kanban@boardgroup
             @\kbtask@columnid\endcsname
           {\csname kanban@count@\kanban@boardgroup
             @\kbtask@columnid\endcsname+1}%
         \csxappto{kanban@list@\kanban@boardgroup
           @\kbtask@columnid}{%
           \noexpand\kanban@rendercard
           {\csuse{kanban@color@\kbtask@columnid}}{\unexpanded{#3}}%
           {\unexpanded\expandafter{\kbtask@prio}}%
           {\unexpanded\expandafter{\kbtask@author}}%
           {\unexpanded\expandafter{\kbtask@deadline}}%
           {\unexpanded\expandafter{\kbtask@late}}%
           {\unexpanded\expandafter{\kbtask@completed}}%
           {\unexpanded\expandafter{\kbtask@bg}}}%
         \expandafter\kanban@diagnosedeadline\expandafter{%
           \kbtask@deadline}%
       }}}
    {\ifbool{kanban@discardtasks}{}
      {\PackageError{kanban-board}
        {task declared outside a kanban environment}
        {Move the task declaration between \string\begin{kanban} and
          \string\end{kanban}. The task was ignored.}}}}
%    \end{macrocode}
%
% \begin{macro}{\kanbantask}
% \begin{macro}{\kbtask}
% The canonical public command supplies the compact task declaration syntax
% and delegates all key processing, validation, and collection to the
% internal implementation. The text argument is long, so a card may contain
% several paragraphs. The option list and the column name remain short,
% because a paragraph break inside them indicates a missing brace, and a
% short argument turns that mistake into an immediate error instead of
% swallowing the rest of the document. The concise command is installed by
% equivalence only when its name is free. An existing definition is
% preserved with a warning.
%    \begin{macrocode}
\NewDocumentCommand\kanbantask{O{}m+m}{%
  \kanban@dotask[#1]{#2}{#3}}
\@ifundefined{kbtask}
  {\let\kbtask\kanbantask}
  {\kanban@warning{alias-taken}{kbtask}{kanbantask}}
%    \end{macrocode}
% \end{macro}
% \end{macro}
%
% \subsection{Board lifecycle}
%
% Board keys update only the active board snapshot. Column setup allocates
% the isolated lists, prepares both rendering sequences, and validates the
% complete gap width before typesetting begins. The entry routine rejects
% nesting before any outer state can be replaced. The environment supplies a
% TeX group, so assignments divided into persistent and local forms acquire
% the documented lifetime without a separate restoration procedure.
%
% \paragraph{Board column selection and board-option keys.}
% A blank board column value means ``use the persistent default''. A
% non-blank value is normalized as a board-local override. The board key
% family provides local counterparts to the persistent setup keys and adds
% the valueless |break| switch. Start and reference dates use the same
% deferred strategy as persistent setup, so key order cannot alter
% ambiguous-date parsing.
%    \begin{macrocode}
\newcommand\kanban@setboardcolumns[1]{%
  \ifblank{#1}
    {\kanban@columnsboardinit}
    {\kanban@selectboardcolumns{#1}}}
\ExplSyntaxOn
\keys_define:nn {kanban/board}
  {
    break .code:n = {\booltrue{kanban@break}},
    break .value_forbidden:n = true,
    columns .code:n = {\kanban@setboardcolumns{#1}},
    date-format .code:n = {\kanban@selectboarddateformat{#1}},
    dateformat .meta:n = {date-format={#1}},
    startdate .code:n =
      {
        \tl_set:Nn \l__kanban_pending_startdate_tl {#1}
        \bool_set_true:N \l__kanban_pending_startdate_bool
      },
    reference-date .code:n =
      {
        \tl_set:Nn \l__kanban_pending_reference_date_tl {#1}
        \bool_set_true:N \l__kanban_pending_reference_date_bool
      },
    today .meta:n = {reference-date={#1}},
    palette .code:n = {\kanban@selectboardpalette{#1}},
    mode .code:n = {\kanban@selectboardmode{#1}},
    columnsep .code:n = {\kanban@setboardcolumnsep{#1}},
    labels .code:n = {\keys_set:nn {kanban/board-labels}{#1}},
    unknown .code:n =
      {\use:x {\msg_warning:nnnn {kanban-board}{unknown-key}
        {board}{\l_keys_key_str}}}
  }
%    \end{macrocode}
%
% \paragraph{A deferred board-option transaction.}
% Board-option processing mirrors persistent setup but commits staged dates
% to the local snapshot setters. All immediate options, including a possible
% |date-format| override, are resolved before the dates are parsed. The
% transaction runs after the persistent values have been copied into the
% board, so omitted keys inherit their defaults.
%    \begin{macrocode}
\cs_new_protected:Npn \kanban@processboardoptions #1
  {
    \bool_set_false:N \l__kanban_pending_startdate_bool
    \bool_set_false:N \l__kanban_pending_reference_date_bool
    \keys_set:nn {kanban/board}{#1}
    \bool_if:NT \l__kanban_pending_startdate_bool
      {\exp_args:NV \kanban@setboardstartdate
        \l__kanban_pending_startdate_tl}
    \bool_if:NT \l__kanban_pending_reference_date_bool
      {\exp_args:NV \kanban@setboardreferencedate
        \l__kanban_pending_reference_date_tl}
  }
\ExplSyntaxOff
%    \end{macrocode}
%
% \paragraph{Per-column state and renderer-sequence construction.}
% Each normalized column is represented internally by a numeric identifier.
% Setup stores the display name, initializes the board-qualified deferred
% task list and counter, assigns the cyclic palette background and
% foreground, and appends one adapter call to each renderer sequence. Card
% lists and counts carry the board number because those definitions are
% global during collection. Later columns in the break sequence are preceded
% by |\switchcolumn| so that |paracol| receives one stream per board column.
%    \begin{macrocode}
\newcommand\kanban@setupcolumn[2]{%
  \csdef{kanban@name@#2}{#1}%
  \csgdef{kanban@list@\kanban@boardgroup @#2}{}%
  \csgdef{kanban@count@\kanban@boardgroup @#2}{0}%
  \kanban@palettecolor{#2}\csedef{kanban@color@#2}{\kb@thiscolor}%
  \csedef{kanban@headingtext@#2}{\kb@thisheadingtext}%
  \numdef\kanban@ncols{\kanban@ncols+1}%
  \eappto\kanban@headseq{\noexpand\kanban@onehead{#2}}%
  \eappto\kanban@colseq{\noexpand\kanban@onecol{#2}}%
  \ifnumequal{#2}{1}
    {\eappto\kanban@colseq@b{\noexpand\kanban@onecol@b{#2}}}
    {\eappto\kanban@colseq@b{\noexpand\switchcolumn
       \noexpand\kanban@onecol@b{#2}}}}
%    \end{macrocode}
%
% \paragraph{Whole-board geometry validation.}
% A syntactically valid non-negative gap may still be impossible for a board
% with many columns. Before rendering, the validator computes the total width
% of the $N-1$ inter-column gaps and requires that total to be strictly
% smaller than |\linewidth|. An impossible value produces an actionable error
% and falls back to zero for this board, which lets the document continue.
%    \begin{macrocode}
\newcommand\kanban@validateboardgeometry{%
  \ifnum\kanban@ncols>1\relax
    \kanban@totalgaps=\kanban@boardcolumnsep
    \multiply\kanban@totalgaps by \numexpr\kanban@ncols-1\relax
    \ifdim\kanban@totalgaps<\linewidth\relax\else
      \PackageError{kanban-board}
        {column separation leaves no width for columns}
        {For N columns, (N-1) times columnsep must be smaller than
          \string\linewidth.\MessageBreak
          The current board will use columnsep=0pt.}%
      \kanban@boardcolumnsep=0pt
    \fi
  \fi}
%    \end{macrocode}
%
% \paragraph{Environment entry and state snapshot.}
% Opening a board either rejects unsupported nesting or creates a complete
% isolated snapshot before tasks are read. A rejected inner environment
% clears its render and active flags locally, sets the discard flag, and
% issues one error, so its body can be read safely without being attached
% to the outer board. The successful path resets the lifecycle flags,
% advances the unique board identifier, copies every persistent scalar and
% list default, processes local overrides, materializes the visual mode,
% clears the renderer sequences, registers all columns, validates the
% aggregate geometry, and suppresses source whitespace before the first
% task.
%    \begin{macrocode}
\newcommand\kanban@begin[1]{%
   \ifbool{kanban@active}
     {\boolfalse{kanban@render}%
      \boolfalse{kanban@active}%
      \booltrue{kanban@discardtasks}%
      \PackageError{kanban-board}
        {nested kanban environments are not supported}
        {Close the current board before opening another one.
          \MessageBreak The nested board was ignored.}}
     {\booltrue{kanban@render}%
   \boolfalse{kanban@break}%
   \boolfalse{kanban@discardtasks}%
   \booltrue{kanban@active}%
   \stepcounter{kanban@board}%
   \edef\kanban@boardgroup{\arabic{kanban@board}}%
   \edef\kanban@thestart{\kanban@startdate}%
   \edef\kanban@boarddateformat{\kanban@dateformat}%
   \edef\kanban@thereference{\kanban@referencedate}%
   \edef\kanban@boardmode{\kanban@mode}%
   \let\kanban@boardhighlabel\kanban@highlabel
   \let\kanban@boardmediumlabel\kanban@mediumlabel
   \let\kanban@boardlowlabel\kanban@lowlabel
   \let\kanban@boarddonelabel\kanban@donelabel
   \kanban@boardcolumnsep=\kanban@columnsep
   \kanban@paletteboardinit
   \kanban@columnsboardinit
   \kanban@processboardoptions{#1}%
   \kanban@applymode
   \def\kanban@ncols{0}\def\kanban@headseq{}%
   \def\kanban@colseq{}\def\kanban@colseq@b{}%
   \kanban@setupboardcolumns
   \kanban@validateboardgeometry
   \ignorespaces}}
%    \end{macrocode}
%
% \begin{macro}{\kanban}
% \begin{macro}{\endkanban}
% The public environment has one optional board-key list and no mandatory
% column argument. Columns come from the persistent default unless a
% board-local |columns| key overrides them. Closing the environment selects
% the page-breakable |paracol| renderer or the indivisible fixed raster. A
% rejected nested environment has its render flag cleared and therefore
% produces no board. Successfully rendered boards release their dynamically
% named state afterward, and the environment group restores all remaining
% local snapshots.
%
% In break mode, the board gap is assigned to the standard |\columnsep| used
% by |paracol|, and the prepared sequence supplies headers, cards, and
% |\switchcolumn| calls. In fixed mode, a full-line |minipage| makes the
% board indivisible. The raster has one column per Kanban stage and two rows,
% all headers followed by all bodies, with equal heights within each row.
%    \begin{macrocode}
\NewDocumentEnvironment{kanban}{O{}}
  {\kanban@begin{#1}}
  {\ifbool{kanban@render}{\par\medskip
   \ifbool{kanban@break}
     {\setlength\columnsep{\kanban@boardcolumnsep}%
      \begin{paracol}{\kanban@ncols}\kanban@colseq@b\end{paracol}}
     {\noindent\begin{minipage}{\linewidth}%
      \begin{tcbraster}[
        raster columns=\kanban@ncols,
        raster rows=2,
        raster width=\linewidth,
        raster column skip=\kanban@boardcolumnsep,
        raster row skip=0pt,
        raster before skip=0pt,
        raster after skip=0pt,
        raster equal height=rows,
        raster valign=top]
        \kanban@headseq\kanban@colseq
      \end{tcbraster}%
      \end{minipage}}%
   \par\vspace{2mm}\kanban@cleanupboard}{}}
%</package>
%    \end{macrocode}
% \end{macro}
% \end{macro}
% \Finale
