% Input:       frozen model records, resolved geometry, metric access, and saved string paths.
% Output:      ink -- nodes and paths on the current tikzpicture.
% Owned state: none; rendering reads records and draws, remembering nothing.
% Invariants:  no parsing, no topology, no recovery; label text never rotates.
% Next stage:  events serialize the same records this stage consumed.
% SPDX-License-Identifier: Apache-2.0
% Copyright the TNLean project; see LICENSE for the full terms.
%
% RENDER-PRIMITIVE LOCKOUT.  This file is loaded LAST by tikz-tensor-networks.sty, and the
% ink primitives below are created here with \cs_new.  Load order documents
% the dependency direction; scripts/tenkz_lint.py --census enforces it by
% rejecting any \__tenkz_ink_ reference outside this file.  (The legacy
% dialect renderers predate the lockout and burn down with stages S2-S4;
% the separate raw-ink census meters watch them.)

\ExplSyntaxOn

\msg_new:nnn {tenkz}{render-wiredir}
  { [TKZ-RENDER-WIREDIR]~wire~record~#1~carries~dir='#2'~but~the~scan~
    derives~'#3';~the~record~is~authoritative }

% No layers: ink order is the fixed class order wires -> atoms -> marks,
% stated here as the stage invariant.  A backgrounds layer is the paint-order
% crutch of token-ordered emitters; a model-ordered renderer never needs one.
% ---------- ink primitives (the only mark-makers) ---------------------------
% #1 tikz option list, #2 x dim expr, #3 y dim expr, #4 node content
% Coordinates stay UNBRACED: a braced component forces the full pgfmath
% parse and loses one scaled point against the direct-dimension shortcut;
% callers never pass top-level commas.
\cs_new_protected:Npn \__tenkz_ink_node:nnnn #1#2#3#4
  { \node[#1] at ( #2 , #3 ) {#4}; }
% #1 tikz option list, #2 full path specification
\cs_new_protected:Npn \__tenkz_ink_path:nn #1#2
  { \draw[#1] #2 ; }

% ---------- coordinator strokes ------------------------------------------------
% The kernel's topology passes decide every stroke; each one is inked here.
% One contraction bond, backed by wire record #1 (diagnostics and, at the
% A coordinator-decided stub or segment: #1 style tokens, #2 path tokens,
% emitted verbatim.
\cs_new_protected:Npn \__tenkz_render_stroke:nn #1#2
  { \__tenkz_ink_path:nn {#1} {#2} }
\cs_generate_variant:Nn \__tenkz_render_stroke:nn { en }
% A coordinator-decided label node: #1 option tokens, #2 position tokens,
% #3 content tokens.
\cs_new_protected:Npn \__tenkz_render_labelnode:nnn #1#2#3
  { \node[#1] at #2 {#3}; }

% ---------- skins ------------------------------------------------------------

% A declared skin's saved own-port route is drawn over its base glyph.  Its
% paper halo both clears the glyph outline and keeps later ordered pairings
% distinct.
\cs_new_protected:Npn \__tenkz_render_skin_pairing:nn #1#2
  {
    \__tenkz_ink_path:nn
      {
        spath/use = {#2} ,
        draw = #1 ,
        line~width = \__tenkz_dim:n {wirewidth} ,
        line~cap = round ,
        preaction =
          {
            draw = tenkzPaper ,
            line~width = \__tenkz_dim:n {emphwidth} ,
            line~cap = butt
          }
      }
      { }
  }

% Draw one atom: #1 skin word, #2 placement id, #3 label.  Coordinates come
% from geometry in pitch units; only here do they meet the pitch dimension.
\tl_new:N \l__tenkz_render_x_tl
\tl_new:N \l__tenkz_render_y_tl
% #4 carries the size the frame asks for.  A glyph belongs to the frame it
% sits on, so a frame that shrinks shrinks its glyphs -- the size is never a
% skin of its own; an unscaled atom passes an empty fourth argument.
% Only `dot` is ever asked for: every other skin reaches paper through
% \__tenkz_kernel_r_atom_glyph:n, which owns the skin-word-to-style table.
% The word stays in the signature so the two call sites read as what they are.
\cs_new_protected:Npn \__tenkz_render_atom_scaled:nnnn #1#2#3#4
  {
    \__tenkz_geom_xy:nNN {#2} \l__tenkz_render_x_tl \l__tenkz_render_y_tl
    \__tenkz_ink_node:nnnn
      { tensor #4 }
      { \l__tenkz_render_x_tl \tenkz@pitch }
      { \l__tenkz_render_y_tl \tenkz@pitch }
      { }
  }

% ---------- labelled atoms ----------------------------------------------------
% #1 placement id, #2 the caller's complete option list (style, hue, name),
% #3 the node content -- the caller owns the tokens, so the typeset body is
% identical to the legacy node's
\cs_new_protected:Npn \__tenkz_render_atom_labelled:nnn #1#2#3
  {
    \__tenkz_geom_xy:nNN {#1} \l__tenkz_render_x_tl \l__tenkz_render_y_tl
    \__tenkz_ink_node:nnnn {#2}
      { \fp_to_decimal:n { \l__tenkz_render_x_tl * \dim_to_fp:n { \tenkz@pitch } } pt }
      { \fp_to_decimal:n { \l__tenkz_render_y_tl * \dim_to_fp:n { \tenkz@pitch } } pt }
      {#3}
  }
\cs_generate_variant:Nn \__tenkz_render_atom_labelled:nnn { nen }

% Measure the exact live node selected by #1 without contributing ink or a
% bounding box.  The caller disables geometry events around this probe.
\cs_new_protected:Npn \__tenkz_render_measure_node:nnn #1#2#3
  {
    \node
      [
        #1 ,
        overlay ,
        draw~opacity = 0 ,
        fill~opacity = 0 ,
        text~opacity = 0
      ]
      (#2) at (0pt,0pt) {#3};
  }

% ---------- labels ------------------------------------------------------------
% Text never rotates.  The anchor is a pure function of the outward angle:
% the label sits beyond the ink along that angle and anchors on its own
% opposite side, in eight compass buckets.
\fp_const:Nn \c__tenkz_render_octant_boundary_fp { 135 / 2 }
\cs_new:Npn \__tenkz_render_label_anchor:n #1
  {
    \fp_compare:nNnTF { cosd(#1) } > { cosd(\c__tenkz_render_octant_boundary_fp) }
      {
        \fp_compare:nNnTF { sind(#1) } > { cosd(\c__tenkz_render_octant_boundary_fp) }
          { south~west }
          {
            \fp_compare:nNnTF { sind(#1) } < { -cosd(\c__tenkz_render_octant_boundary_fp) }
              { north~west } { west }
          }
      }
      {
        \fp_compare:nNnTF { cosd(#1) } < { -cosd(\c__tenkz_render_octant_boundary_fp) }
          {
            \fp_compare:nNnTF { sind(#1) } > { cosd(\c__tenkz_render_octant_boundary_fp) }
              { south~east }
              {
                \fp_compare:nNnTF
                  { sind(#1) } < { -cosd(\c__tenkz_render_octant_boundary_fp) }
                  { north~east } { east }
              }
          }
          { \fp_compare:nNnTF { sind(#1) } > { 0 } { south } { north } }
      }
  }

% ---------- strings -----------------------------------------------------------
% A string draws its saved spath3 route: after crossing surgery the saved
% path already carries its gaps, so one stroke is the whole story.  #1 the
% saved path id, #2 extra style tokens (species hue, emphasis).
\cs_new_protected:Npn \__tenkz_render_string:nn #1#2
  {
    \__tenkz_ink_path:nn
      { spath/use = {#1} , line~width = \__tenkz_dim:n {wirewidth} , #2 }
      { }
  }

\ExplSyntaxOff
\endinput
