-- |
-- Module      : Harmonic.Framework.Builder.Strata
-- Description : Strata-first traversal helpers for the 'Harmonic.Framework.Builder.genP' paradigm
--
-- Helpers that decide which (strata, tristrata) pair bar @i@ is drawn from,
-- given the allowed tristrata list, the previous bar's assignment, and
-- optional per-bar constraints ('Harmonic.Framework.Builder.relStrata' \/ 'Harmonic.Framework.Builder.absStrata'). Pure functions;
-- no IO, no randomness.

module Harmonic.Framework.Builder.Strata
  ( adjacentInTristrata
  , allowedNext
  , initialPlacement
  , modeForTriad
  , pickPartner
  , positionalPartner
  , selectNext
  , selectNextSeeded
  ) where

import Data.List (find, nub, sortBy)
import Data.Ord (comparing)
import Data.Maybe (listToMaybe)

import qualified Harmonic.Rules.Types.Scale as Sc
import Harmonic.Rules.Types.Scale
  ( StrataLabel, Tristrata(..), Mode(..), ModeQuality(..), ModeResult(..)
  , tristrataStrataAt, tristrataOf, tristrataDissonance
  , strataDissonance, strataChroma, classifyModeAt
  )
import Harmonic.Rules.Types.Pitch (PitchClass(..), mkPitchClass)

-- |Position of a strata inside a tristrata (1, 2, or 3); 'Nothing' if the
-- strata is not a member of that tristrata.
positionOf :: Tristrata -> StrataLabel -> Maybe Int
positionOf :: Tristrata -> StrataLabel -> Maybe Int
positionOf Tristrata
t StrataLabel
s
  | Tristrata -> StrataLabel
ts1 Tristrata
t StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
== StrataLabel
s = Int -> Maybe Int
forall a. a -> Maybe a
Just Int
1
  | Tristrata -> StrataLabel
ts2 Tristrata
t StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
== StrataLabel
s = Int -> Maybe Int
forall a. a -> Maybe a
Just Int
2
  | Tristrata -> StrataLabel
ts3 Tristrata
t StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
== StrataLabel
s = Int -> Maybe Int
forall a. a -> Maybe a
Just Int
3
  | Bool
otherwise  = Maybe Int
forall a. Maybe a
Nothing

-- |In-tristrata circular neighbourhood of a strata: positions @{p-1, p, p+1}@
-- mod 3 in the tristrata @t@. The current strata is included so a bar may
-- repeat the previous strata within the same tristrata.
adjacentInTristrata :: Tristrata -> StrataLabel -> [StrataLabel]
adjacentInTristrata :: Tristrata -> StrataLabel -> [StrataLabel]
adjacentInTristrata Tristrata
t StrataLabel
s = case Tristrata -> StrataLabel -> Maybe Int
positionOf Tristrata
t StrataLabel
s of
  Maybe Int
Nothing -> []
  Just Int
p  ->
    let positions :: [Int]
positions = [ ((Int
p Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
2) Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` Int
3) Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
                    , Int
p
                    , (Int
p Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` Int
3) Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
                    ]
    in (Int -> StrataLabel) -> [Int] -> [StrataLabel]
forall a b. (a -> b) -> [a] -> [b]
map (Tristrata -> Int -> StrataLabel
tristrataStrataAt Tristrata
t) [Int]
positions

-- |Complete viable @(strata', tristrata')@ candidate set for a given
-- @(strata, tristrata)@ step, respecting the allowed-tristrata allow-list.
--
-- For every @(t', p')@ in @'tristrataOf' s@ where @t'@ is allowed, emit every
-- @(s', t')@ with @s'@ drawn from @'adjacentInTristrata' t' s@.
allowedNext :: [Tristrata] -> (StrataLabel, Tristrata) -> [(StrataLabel, Tristrata)]
allowedNext :: [Tristrata]
-> (StrataLabel, Tristrata) -> [(StrataLabel, Tristrata)]
allowedNext [Tristrata]
allowed (StrataLabel
s, Tristrata
_t) =
  [ (StrataLabel
s', Tristrata
t')
  | (Tristrata
t', Int
_p') <- StrataLabel -> [(Tristrata, Int)]
tristrataOf StrataLabel
s
  , Tristrata
t' Tristrata -> [Tristrata] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Tristrata]
allowed
  , StrataLabel
s' <- Tristrata -> StrataLabel -> [StrataLabel]
adjacentInTristrata Tristrata
t' StrataLabel
s
  ]

-- |Initial @(strata, tristrata)@ placement: use the supplied starting strata,
-- and pick the lowest-dissonance allowed tristrata that contains it. If no
-- allowed tristrata contains the strata, fall back to the first allowed one
-- (edge case — caller should usually avoid this via a sensible default).
initialPlacement :: [Tristrata] -> StrataLabel -> (StrataLabel, Tristrata)
initialPlacement :: [Tristrata] -> StrataLabel -> (StrataLabel, Tristrata)
initialPlacement [Tristrata]
allowed StrataLabel
s =
  case (Tristrata -> Tristrata -> Ordering) -> [Tristrata] -> [Tristrata]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy ((Tristrata -> Int) -> Tristrata -> Tristrata -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing Tristrata -> Int
tristrataDissonance)
       [ Tristrata
t | (Tristrata
t, Int
_) <- StrataLabel -> [(Tristrata, Int)]
tristrataOf StrataLabel
s, Tristrata
t Tristrata -> [Tristrata] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Tristrata]
allowed ] of
    (Tristrata
t:[Tristrata]
_) -> (StrataLabel
s, Tristrata
t)
    []    -> case [Tristrata]
allowed of
               (Tristrata
t:[Tristrata]
_) -> (StrataLabel
s, Tristrata
t)
               []    -> (StrataLabel
s, Int -> Tristrata
Sc.tristrataIndex Int
1)  -- extreme edge case

-- |Bar-0 \/ no-prior starter rule: pick a fixed partner strata inside the
-- given tristrata based on the current strata's position.
--
--   * pos 1 → ts2
--   * pos 2 → ts1
--   * pos 3 → ts1
--
-- Yields a 7-PC pair-union (which is what the framework guarantees inside a
-- single tristrata).
positionalPartner :: Tristrata -> StrataLabel -> StrataLabel
positionalPartner :: Tristrata -> StrataLabel -> StrataLabel
positionalPartner Tristrata
t StrataLabel
s = case Tristrata -> StrataLabel -> Maybe Int
positionOf Tristrata
t StrataLabel
s of
  Just Int
1 -> Tristrata -> StrataLabel
ts2 Tristrata
t
  Just Int
2 -> Tristrata -> StrataLabel
ts1 Tristrata
t
  Just Int
3 -> Tristrata -> StrataLabel
ts1 Tristrata
t
  Maybe Int
_      -> Tristrata -> StrataLabel
ts2 Tristrata
t

-- |Choose a partner strata for the union construction at bar @i@.
--
-- Preference order:
--
--   1. The /most recent/ prior bar whose strata @≠ s_curr@ — "last
--      different" (preserves the user's intended behaviour for
--      mid-run bars where the mode reflects the incoming transition).
--   2. If no different prior bar exists (bar 0, or the entire history
--      so far shares @s_curr@), look /forward/ for the first future
--      bar whose strata @≠ s_curr@ — the "first @next@ different" rule.
--      This lets bar 0's mode reflect the /outgoing/ transition to the
--      next distinct strata, which is musically more informative than
--      the positional fallback.
--   3. Otherwise (single-strata run), fall back to 'positionalPartner'
--      inside the current bar's tristrata.
pickPartner :: [(StrataLabel, Tristrata)]   -- ^ full bar sequence (0-indexed)
            -> Int                          -- ^ current bar index
            -> StrataLabel                  -- ^ current strata
            -> Tristrata                    -- ^ current tristrata (used for fallback)
            -> StrataLabel
pickPartner :: [(StrataLabel, Tristrata)]
-> Int -> StrataLabel -> Tristrata -> StrataLabel
pickPartner [(StrataLabel, Tristrata)]
barSeq Int
i StrataLabel
sCurr Tristrata
tCurr =
  case Maybe StrataLabel
lastDifferent of
    Just StrataLabel
sLast -> StrataLabel
sLast
    Maybe StrataLabel
Nothing    -> case Maybe StrataLabel
firstForwardDifferent of
      Just StrataLabel
sNext -> StrataLabel
sNext
      Maybe StrataLabel
Nothing    -> Tristrata -> StrataLabel -> StrataLabel
positionalPartner Tristrata
tCurr StrataLabel
sCurr
  where
    lastDifferent :: Maybe StrataLabel
lastDifferent =
      [StrataLabel] -> Maybe StrataLabel
forall a. [a] -> Maybe a
listToMaybe
        [ StrataLabel
s
        | Int
j <- [Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1, Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
2 .. Int
0]
        , Int
j Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
0
        , let (StrataLabel
s, Tristrata
_) = [(StrataLabel, Tristrata)]
barSeq [(StrataLabel, Tristrata)] -> Int -> (StrataLabel, Tristrata)
forall a. HasCallStack => [a] -> Int -> a
!! Int
j
        , StrataLabel
s StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
/= StrataLabel
sCurr
        ]
    firstForwardDifferent :: Maybe StrataLabel
firstForwardDifferent =
      [StrataLabel] -> Maybe StrataLabel
forall a. [a] -> Maybe a
listToMaybe
        [ StrataLabel
s
        | Int
j <- [Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1 .. [(StrataLabel, Tristrata)] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(StrataLabel, Tristrata)]
barSeq Int -> Int -> Int
forall a. Num a => a -> a -> a
- Int
1]
        , let (StrataLabel
s, Tristrata
_) = [(StrataLabel, Tristrata)]
barSeq [(StrataLabel, Tristrata)] -> Int -> (StrataLabel, Tristrata)
forall a. HasCallStack => [a] -> Int -> a
!! Int
j
        , StrataLabel
s StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
/= StrataLabel
sCurr
        ]

-- |Triad-anchored, history-aware mode selector for bar @i@.
--
-- Walks @barSeq@ backward from @i-1@ to find the most recent strata
-- @≠ barSeq[i].strata@; if none, uses 'positionalPartner' on
-- @barSeq[i].tristrata@. Unions @strataChroma sCurr@ with the partner's
-- chroma, then classifies via 'classifyModeAt' pinned to @triadRootPC@.
--
-- Returns 'ModeOk' for a normal classification; 'ModeInvalid' when the
-- union doesn't have exactly 7 unique pitch classes (only reachable via
-- 'Harmonic.Framework.Builder.absStrata' overrides that violate tristrata adjacency). When the
-- 7-PC union doesn't classify under any of the 28 mode patterns pinned
-- to @triadRootPC@, returns @ModeOk (Mode Aeolian (P triadRootPC))@ as
-- a benign fallback.
modeForTriad :: [(StrataLabel, Tristrata)]
             -> Int
             -> Int                          -- ^ triad's harmonic root PC (0..11)
             -> ModeResult
modeForTriad :: [(StrataLabel, Tristrata)] -> Int -> Int -> ModeResult
modeForTriad [(StrataLabel, Tristrata)]
barSeq Int
i Int
triadRootPC
  | Int
i Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
< Int
0 Bool -> Bool -> Bool
|| Int
i Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= [(StrataLabel, Tristrata)] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(StrataLabel, Tristrata)]
barSeq = [PitchClass] -> ModeResult
ModeInvalid []
  | Bool
otherwise =
      let (StrataLabel
sCurr, Tristrata
tCurr) = [(StrataLabel, Tristrata)]
barSeq [(StrataLabel, Tristrata)] -> Int -> (StrataLabel, Tristrata)
forall a. HasCallStack => [a] -> Int -> a
!! Int
i
          partnerStrata :: StrataLabel
partnerStrata  = [(StrataLabel, Tristrata)]
-> Int -> StrataLabel -> Tristrata -> StrataLabel
pickPartner [(StrataLabel, Tristrata)]
barSeq Int
i StrataLabel
sCurr Tristrata
tCurr
          unionPCs :: [PitchClass]
unionPCs       = [PitchClass] -> [PitchClass]
forall a. Eq a => [a] -> [a]
nub (StrataLabel -> [PitchClass]
strataChroma StrataLabel
sCurr [PitchClass] -> [PitchClass] -> [PitchClass]
forall a. [a] -> [a] -> [a]
++ StrataLabel -> [PitchClass]
strataChroma StrataLabel
partnerStrata)
      in if [PitchClass] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [PitchClass]
unionPCs Int -> Int -> Bool
forall a. Eq a => a -> a -> Bool
/= Int
7
           then [PitchClass] -> ModeResult
ModeInvalid [PitchClass]
unionPCs
           else case Int -> [PitchClass] -> Maybe Mode
classifyModeAt Int
triadRootPC [PitchClass]
unionPCs of
                  Just Mode
m  -> Mode -> ModeResult
ModeOk Mode
m
                  Maybe Mode
Nothing -> Mode -> ModeResult
ModeOk (ModeQuality -> PitchClass -> Mode
Mode ModeQuality
Aeolian (Int -> PitchClass
mkPitchClass Int
triadRootPC))

-- |Select the next @(s', t')@ from a candidate pool using the tie-break rule:
--
--   1. Prefer @s' == s_{prev}@ (strata continuity); within that, prefer
--      @t' == t_{prev}@ if still valid, otherwise lowest-dissonance tristrata.
--   2. Prefer @t' == t_{prev}@ (tristrata continuity) — lowest-dissonance
--      option inside the previous tristrata wins over a more-consonant strata
--      in another tristrata.
--   3. Otherwise pick by @('strataDissonance' s', 'tristrataDissonance' t')@.
--
-- Returns 'Nothing' on empty pool.
selectNext :: (StrataLabel, Tristrata)
           -> [(StrataLabel, Tristrata)]
           -> Maybe (StrataLabel, Tristrata)
selectNext :: (StrataLabel, Tristrata)
-> [(StrataLabel, Tristrata)] -> Maybe (StrataLabel, Tristrata)
selectNext (StrataLabel, Tristrata)
_ []   = Maybe (StrataLabel, Tristrata)
forall a. Maybe a
Nothing
selectNext (StrataLabel
sPrev, Tristrata
tPrev) [(StrataLabel, Tristrata)]
pool =
  let sameStrata :: [(StrataLabel, Tristrata)]
sameStrata     = ((StrataLabel, Tristrata) -> Bool)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> Bool) -> [a] -> [a]
filter (\(StrataLabel
s, Tristrata
_) -> StrataLabel
s StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
== StrataLabel
sPrev) [(StrataLabel, Tristrata)]
pool
      sameTristrata :: [(StrataLabel, Tristrata)]
sameTristrata  = ((StrataLabel, Tristrata) -> Bool)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> Bool) -> [a] -> [a]
filter (\(StrataLabel
_, Tristrata
t) -> Tristrata
t Tristrata -> Tristrata -> Bool
forall a. Eq a => a -> a -> Bool
== Tristrata
tPrev) [(StrataLabel, Tristrata)]
pool
      byDissonance :: [(StrataLabel, Tristrata)]
byDissonance   = ((StrataLabel, Tristrata) -> (StrataLabel, Tristrata) -> Ordering)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy (((StrataLabel, Tristrata) -> (Int, Int))
-> (StrataLabel, Tristrata) -> (StrataLabel, Tristrata) -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (StrataLabel, Tristrata) -> (Int, Int)
keyDiss) [(StrataLabel, Tristrata)]
pool
      keyDiss :: (StrataLabel, Tristrata) -> (Int, Int)
keyDiss (StrataLabel
s, Tristrata
t) = (StrataLabel -> Int
strataDissonance StrataLabel
s, Tristrata -> Int
tristrataDissonance Tristrata
t)
  in case [(StrataLabel, Tristrata)]
sameStrata of
       ((StrataLabel, Tristrata)
_:[(StrataLabel, Tristrata)]
_) ->
         -- Within same-strata, prefer t' == tPrev, else lowest tristrata dissonance.
         case ((StrataLabel, Tristrata) -> Bool)
-> [(StrataLabel, Tristrata)] -> Maybe (StrataLabel, Tristrata)
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Maybe a
find (\(StrataLabel
_, Tristrata
t) -> Tristrata
t Tristrata -> Tristrata -> Bool
forall a. Eq a => a -> a -> Bool
== Tristrata
tPrev) [(StrataLabel, Tristrata)]
sameStrata of
           Just (StrataLabel, Tristrata)
hit -> (StrataLabel, Tristrata) -> Maybe (StrataLabel, Tristrata)
forall a. a -> Maybe a
Just (StrataLabel, Tristrata)
hit
           Maybe (StrataLabel, Tristrata)
Nothing  -> [(StrataLabel, Tristrata)] -> Maybe (StrataLabel, Tristrata)
forall a. [a] -> Maybe a
listToMaybe (((StrataLabel, Tristrata) -> (StrataLabel, Tristrata) -> Ordering)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy (((StrataLabel, Tristrata) -> Int)
-> (StrataLabel, Tristrata) -> (StrataLabel, Tristrata) -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (Tristrata -> Int
tristrataDissonance (Tristrata -> Int)
-> ((StrataLabel, Tristrata) -> Tristrata)
-> (StrataLabel, Tristrata)
-> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (StrataLabel, Tristrata) -> Tristrata
forall a b. (a, b) -> b
snd)) [(StrataLabel, Tristrata)]
sameStrata)
       [] -> case [(StrataLabel, Tristrata)]
sameTristrata of
               ((StrataLabel, Tristrata)
_:[(StrataLabel, Tristrata)]
_) -> [(StrataLabel, Tristrata)] -> Maybe (StrataLabel, Tristrata)
forall a. [a] -> Maybe a
listToMaybe (((StrataLabel, Tristrata) -> (StrataLabel, Tristrata) -> Ordering)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> a -> Ordering) -> [a] -> [a]
sortBy (((StrataLabel, Tristrata) -> (Int, Int))
-> (StrataLabel, Tristrata) -> (StrataLabel, Tristrata) -> Ordering
forall a b. Ord a => (b -> a) -> b -> b -> Ordering
comparing (StrataLabel, Tristrata) -> (Int, Int)
keyDiss) [(StrataLabel, Tristrata)]
sameTristrata)
               []    -> [(StrataLabel, Tristrata)] -> Maybe (StrataLabel, Tristrata)
forall a. [a] -> Maybe a
listToMaybe [(StrataLabel, Tristrata)]
byDissonance

-- |Seeded stochastic variant of 'selectNext'. Permits the walk to
-- actually traverse — the categorical same-strata-first rule of
-- 'selectNext' locks the walk to its starting strata indefinitely,
-- which is musically sterile.
--
-- Given an @Int@ sample drawn from an RNG by the caller, this picks a
-- candidate from @pool@ with the following preference order:
--
--   1. Prefer candidates whose strata @≠ sPrev@ (forces exploration).
--      Among those, prefer ones sharing @tPrev@ (tristrata continuity)
--      over those in other tristrata.
--   2. Fall back to the self-loop only when the pool contains nothing
--      else (rare — happens under extreme @hcTristrata@ \/ @relStrata@
--      narrowing).
--
-- Within the preferred group, the sample index (mod group size) picks
-- the concrete candidate. This gives run-to-run variety while
-- respecting 'Harmonic.Framework.Builder.genP's continuity intent (staying inside the current
-- tristrata's harmonic region when possible).
selectNextSeeded :: Int                               -- ^ rng sample (any Int)
                 -> (StrataLabel, Tristrata)          -- ^ prior bar
                 -> [(StrataLabel, Tristrata)]        -- ^ candidate pool
                 -> Maybe (StrataLabel, Tristrata)
selectNextSeeded :: Int
-> (StrataLabel, Tristrata)
-> [(StrataLabel, Tristrata)]
-> Maybe (StrataLabel, Tristrata)
selectNextSeeded Int
_    (StrataLabel, Tristrata)
_             []   = Maybe (StrataLabel, Tristrata)
forall a. Maybe a
Nothing
selectNextSeeded Int
seed (StrataLabel
sPrev, Tristrata
tPrev) [(StrataLabel, Tristrata)]
pool =
  let nonSelf :: [(StrataLabel, Tristrata)]
nonSelf     = ((StrataLabel, Tristrata) -> Bool)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> Bool) -> [a] -> [a]
filter (\(StrataLabel
s, Tristrata
_) -> StrataLabel
s StrataLabel -> StrataLabel -> Bool
forall a. Eq a => a -> a -> Bool
/= StrataLabel
sPrev) [(StrataLabel, Tristrata)]
pool
      sameTri :: [(StrataLabel, Tristrata)]
sameTri     = ((StrataLabel, Tristrata) -> Bool)
-> [(StrataLabel, Tristrata)] -> [(StrataLabel, Tristrata)]
forall a. (a -> Bool) -> [a] -> [a]
filter (\(StrataLabel
_, Tristrata
t) -> Tristrata
t Tristrata -> Tristrata -> Bool
forall a. Eq a => a -> a -> Bool
== Tristrata
tPrev) [(StrataLabel, Tristrata)]
nonSelf
      group :: [(StrataLabel, Tristrata)]
group       = if Bool -> Bool
not ([(StrataLabel, Tristrata)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(StrataLabel, Tristrata)]
sameTri)
                      then [(StrataLabel, Tristrata)]
sameTri
                      else if Bool -> Bool
not ([(StrataLabel, Tristrata)] -> Bool
forall a. [a] -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null [(StrataLabel, Tristrata)]
nonSelf)
                             then [(StrataLabel, Tristrata)]
nonSelf
                             else [(StrataLabel, Tristrata)]
pool        -- self-loop fallback
      idx :: Int
idx         = (Int -> Int
forall a. Num a => a -> a
abs Int
seed) Int -> Int -> Int
forall a. Integral a => a -> a -> a
`mod` [(StrataLabel, Tristrata)] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [(StrataLabel, Tristrata)]
group
  in (StrataLabel, Tristrata) -> Maybe (StrataLabel, Tristrata)
forall a. a -> Maybe a
Just ([(StrataLabel, Tristrata)]
group [(StrataLabel, Tristrata)] -> Int -> (StrataLabel, Tristrata)
forall a. HasCallStack => [a] -> Int -> a
!! Int
idx)