(* =====================================================================
   21_fb_duty_selector.st - FB_DUTY_SELECT, section 4.2

   Decides WHICH units run, never HOW MANY.

   Ranking, best first:
     1. unavailable units excluded entirely
     2. not-due-for-service ranks above due-for-service
     3. within a group, lower run hours first
     4. ties break by ascending pump number (deterministic for demos)

   The two rules that outrank the ranking:
     - never stop a running unit to start a better-ranked one
     - service-due is a preference, never a veto
   ===================================================================== *)

FUNCTION_BLOCK FB_DUTY_SELECT
VAR_INPUT
    Available       : ARRAY[1..3] OF BOOL;
    RunHours        : ARRAY[1..3] OF REAL;
    ServiceDue      : ARRAY[1..3] OF BOOL;
    RunningNow      : ARRAY[1..3] OF BOOL;
    PumpsRequired   : INT;
END_VAR

VAR_OUTPUT
    RunRequest      : ARRAY[1..3] OF BOOL;
    DutyPump        : INT;      (* 0 = none, 1-3 *)
END_VAR

VAR CONSTANT
    (* Service-due penalty.  Larger than any credible run-hours value,
       so a due unit always ranks below every not-due unit while still
       remaining in the list - rule 2 is a preference, not a veto. *)
    SERVICE_PENALTY : REAL := 1000000.0;
END_VAR

VAR
    Rank    : ARRAY[1..3] OF INT;   (* pump numbers, best first *)
    Used    : ARRAY[1..3] OF BOOL;
    Sel     : ARRAY[1..3] OF BOOL;
    i       : INT;
    k       : INT;
    best    : INT;
    bestKey : REAL;
    key     : REAL;
    nRanked : INT;
    slots   : INT;
    cnt     : INT;
END_VAR

(* --- 1. Reset working state --------------------------------------- *)
FOR i := 1 TO 3 DO
    Used[i] := FALSE;
    Sel[i]  := FALSE;
    Rank[i] := 0;
END_FOR;
nRanked := 0;

(* --- 2. Build the ranked list by repeated selection.
       Scanning i ascending with a strict "<" test means an equal key
       never displaces an earlier pump, which is rule 4. ------------ *)
FOR k := 1 TO 3 DO
    best    := 0;
    bestKey := 0.0;
    FOR i := 1 TO 3 DO
        IF Available[i] AND NOT Used[i] THEN
            key := RunHours[i];
            IF ServiceDue[i] THEN
                key := key + SERVICE_PENALTY;
            END_IF;
            IF (best = 0) OR (key < bestKey) THEN
                best    := i;
                bestKey := key;
            END_IF;
        END_IF;
    END_FOR;
    IF best > 0 THEN
        nRanked       := nRanked + 1;
        Rank[nRanked] := best;
        Used[best]    := TRUE;
    END_IF;
END_FOR;

(* --- 3. Clamp the demand ------------------------------------------ *)
slots := PumpsRequired;
IF slots < 0 THEN
    slots := 0;
END_IF;
IF slots > 3 THEN
    slots := 3;
END_IF;

(* --- 4. Pass A: units already running keep their slots.
       Walking in rank order means that if the demand has dropped it
       is the worst-ranked running unit that loses its slot. -------- *)
cnt := 0;
FOR k := 1 TO nRanked DO
    i := Rank[k];
    IF RunningNow[i] AND (cnt < slots) THEN
        Sel[i] := TRUE;
        cnt    := cnt + 1;
    END_IF;
END_FOR;

(* --- 5. Pass B: fill the slots that remain, by rank --------------- *)
FOR k := 1 TO nRanked DO
    i := Rank[k];
    IF (NOT Sel[i]) AND (cnt < slots) THEN
        Sel[i] := TRUE;
        cnt    := cnt + 1;
    END_IF;
END_FOR;

(* --- 6. Publish.  DutyPump is the best-ranked selected unit. ------- *)
DutyPump := 0;
FOR k := 1 TO nRanked DO
    i := Rank[k];
    IF Sel[i] AND (DutyPump = 0) THEN
        DutyPump := i;
    END_IF;
END_FOR;

FOR i := 1 TO 3 DO
    RunRequest[i] := Sel[i];
END_FOR;

END_FUNCTION_BLOCK
