package puzzle;

import module java.base;
import anno.GenerateShapedCopies;
import anno.Shaped;
import lombok.val;
import precomp.Neighbors9x8;
import gen.rci;

import static java.lang.Long.*;
import static puzzle.SwedishGenerator.*;
@GenerateShapedCopies(
        packageName = "puzzle",
        className = "Masker",
        shapes = { "precomp.Neighbors9x8", "precomp.Neighbors4x3" }
)
public final class Masker {
   
   @Shaped public static final  int     SIZE       = Neighbors9x8.SIZE;
   @Shaped public static final  rci[]   IT         = Neighbors9x8.IT;
   @Shaped public static final  long[]  PATH_LO    = Neighbors9x8.PATH_LO;
   @Shaped public static final  long[]  PATH_HI    = Neighbors9x8.PATH_HI;
   @Shaped public static final  long    MASK_LO    = Neighbors9x8.MASK_LO;
   @Shaped public static final  long    MASK_HI    = Neighbors9x8.MASK_HI;//(1L << (SIZE - 64)) - 1;
   @Shaped public static final  int     MIN_LEN    = Neighbors9x8.MIN_LEN;//Config.MIN_LEN;
   @Shaped public static final  int     C          = Neighbors9x8.C;
   @Shaped public static final  int     R          = Neighbors9x8.R;
   @Shaped public static final  double  SIZED      = Neighbors9x8.SIZED;// ~18
   @Shaped private static final long[]  NBR_LO     = Neighbors9x8.NBR_LO;
   @Shaped private static final long[]  NBR_HI     = Neighbors9x8.NBR_HI;
   public static final          int[][] MUTATE_RI  = new int[SIZE][625];
   private static final         boolean VERBOSE    = false;
   private final                int[]   activeCIdx = new int[SIZE];
   private final                long[]  activeSLo  = new long[SIZE];
   private final                long[]  activeSHi  = new long[SIZE];
   private final                long[]  adjLo      = new long[SIZE];
   private final                long[]  adjHi      = new long[SIZE];
   private final                int[]   rCount     = new int[R];
   private final                int[]   cCount     = new int[C];
   private final                Rng     rng;
   private final                int[]   stack;
   private final                Clues   cache;
   public static final          int     STACK_SIZE = 128;
   public Masker(Rng rng, int[] stack, Clues cache) {
      this.rng   = rng;
      this.stack = stack;
      this.cache = cache;
   }
   public static boolean isLo(int n) { return (n & 64) == 0; }
   public static double similarity(Clues a, Clues b) {
      var matchLo = (~(a.lo ^ b.lo)) & (~a.lo | (~(a.vlo ^ b.vlo) & ~(a.rlo ^ b.rlo) & ~(a.xlo ^ b.xlo)));
      var matchHi = (~(a.hi ^ b.hi)) & (~a.hi | (~(a.vhi ^ b.vhi) & ~(a.rhi ^ b.rhi) & ~(a.xhi ^ b.xhi)));
      
      return (bitCount(matchLo & MASK_LO) + bitCount(matchHi & MASK_HI)) / SIZED;
   }
   
   public boolean isValid(Clues c) {
      return findOffendingClue(c) == -1;
   }
   
   public int findOffendingClue(Clues c) {
      if (((c.xlo & c.rlo) & c.lo) != X) return numberOfTrailingZeros((c.xlo & c.rlo) & c.lo);
      if (((c.xhi & c.rhi) & c.hi) != X) return 64 | numberOfTrailingZeros((c.xhi & c.rhi) & c.hi);
      
      var num = 0;
      for (var bits = c.lo; bits != X; bits &= bits - 1) activeCIdx[num++] = numberOfTrailingZeros(bits);
      for (var bits = c.hi; bits != X; bits &= bits - 1) activeCIdx[num++] = 64 | numberOfTrailingZeros(bits);
      
      if (num == 0) return -1;
      
      var start = rng.randint0_SIZE() % num;
      var n     = 0;
      for (var i = 0; i < num; i++) {
         var  idx = activeCIdx[(start + i) % num];
         int  dir = c.getDir(idx);
         var  key = Slot.packSlotKey(idx, dir);
         long sLo = PATH_LO[key], sHi = PATH_HI[key];
         long hLo = sLo & c.lo, hHi = sHi & c.hi;
         if (Slotinfo.increasing(key)) {
            if (hLo != X) {
               sLo &= (1L << numberOfTrailingZeros(hLo)) - 1;
               sHi = 0;
            } else if (hHi != X) { sHi &= (1L << numberOfTrailingZeros(hHi)) - 1; }
         } else {
            if (hHi != X) {
               sHi &= -(1L << (63 - numberOfLeadingZeros(hHi)) << 1);
               sLo = 0;
            } else if (hLo != X) { sLo &= -(1L << (63 - numberOfLeadingZeros(hLo)) << 1); }
         }
         if (bitCount(sLo) + bitCount(sHi) < MIN_LEN) return idx;
         for (var j = 0; j < n; j++) if (bitCount(sLo & activeSLo[j]) + bitCount(sHi & activeSHi[j]) > 1) return idx;
         activeSLo[n] = sLo;
         activeSHi[n] = sHi;
         n++;
      }
      return -1;
   }
   public static Grid toGrid(Clues c) { return new Grid(new byte[SIZE], c.lo, c.hi); }
   public void cleanup(Clues c) {
      var guard = 0;
      while (guard++ < 50) {
         var offending = findOffendingClue(c);
         if (offending == -1) break;
         if ((offending & 64) == 0) c.clearClueLo(~(1L << offending));
         else c.clearClueHi(~(1L << (offending & 63)));
      }
   }
   
   static {
      for (var i = 0; i < SIZE; i++) {
         var k = 0;
         for (var dr1 = -2; dr1 <= 2; dr1++)
            for (var dr2 = -2; dr2 <= 2; dr2++)
               for (var dc1 = -2; dc1 <= 2; dc1++)
                  for (var dc2 = -2; dc2 <= 2; dc2++) {
                     val ti = IT[i];
                     MUTATE_RI[i][k++] = offset(clamp(ti.r() + dr1 + dr2, 0, R - 1),
                                                clamp(ti.c() + dc1 + dc2, 0, C - 1));
                  }
      }
   }
   // slice ray to stop before first clue, depending on direction monotonicity
   // right/down => increasing indices; up/left => decreasing indices
   // first clue is highest index among hits (hi first, then lo)
   static void processSlotRev(Clues c, SlotVisitor visitor, int key) {
      var rayLo = PATH_LO[key];
      var rayHi = PATH_HI[key];
      // only consider clue cells
      var hitsLo = rayLo & c.lo;
      var hitsHi = rayHi & c.hi;
      
      if (hitsHi != X) {
         var msb  = 63 - numberOfLeadingZeros(hitsHi);
         var stop = 1L << msb;
         rayHi &= -(stop << 1); // keep bits > stop
         rayLo = 0;                   // lo indices are below stop
      } else if (hitsLo != X) {
         var msb  = 63 - numberOfLeadingZeros(hitsLo);
         var stop = 1L << msb;
         rayLo &= -(stop << 1);
      }
      if (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN)
         visitor.visit(key, rayLo, rayHi);
   }
   static boolean validSlotRev(long lo, long hi, int key) {
      var rayLo = PATH_LO[key];
      var rayHi = PATH_HI[key];
      // only consider clue cells
      var hitsLo = rayLo & lo;
      var hitsHi = rayHi & hi;
      
      if (hitsHi != X) return (Long.bitCount(rayHi & -(1L << 63 - numberOfLeadingZeros(hitsHi) << 1)) >= MIN_LEN);
      else if (hitsLo != X) return (Long.bitCount(rayLo & -(1L << 63 - numberOfLeadingZeros(hitsLo) << 1)) + Long.bitCount(rayHi) >= MIN_LEN);
      else return (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN);
   }
   static boolean validSlot(long lo, long hi, int key) {
      var rayLo  = PATH_LO[key];
      var rayHi  = PATH_HI[key];
      var hitsLo = rayLo & lo;
      var hitsHi = rayHi & hi;
      
      if (hitsLo != X) return (Long.bitCount(rayLo & ((1L << numberOfTrailingZeros(hitsLo)) - 1)) >= MIN_LEN);
      else if (hitsHi != X) return (Long.bitCount(rayLo) + Long.bitCount(rayHi & ((1L << numberOfTrailingZeros(hitsHi)) - 1)) >= MIN_LEN);
      else return (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN);
   }
   static void processSlot(Clues c, SlotVisitor visitor, int key) {
      var rayLo  = PATH_LO[key];
      var rayHi  = PATH_HI[key];
      var hitsLo = rayLo & c.lo;
      var hitsHi = rayHi & c.hi;
      
      if (hitsLo != X) {
         var stop = 1L << numberOfTrailingZeros(hitsLo);
         rayLo &= (stop - 1);
         rayHi = 0;
      } else if (hitsHi != X) {
         var stop = 1L << numberOfTrailingZeros(hitsHi);
         rayHi &= (stop - 1);
      }
      if (Long.bitCount(rayLo) + Long.bitCount(rayHi) >= MIN_LEN)
         visitor.visit(key, rayLo, rayHi);
   }
   public static  void forEachSlot(Clues c,SlotVisitor visitor) {
      final long lo = c.lo, hi = c.hi, xlo = c.xlo, xhi = c.xhi, rlo = c.rlo, rhi = c.rhi, vlo = c.vlo, vhi = c.vhi;
      for (var l = lo & ~xlo & ~rlo & vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 1));
      for (var l = lo & ~xlo & ~rlo & ~vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 0));
      for (var l = lo & ~xlo & rlo & ~vlo; l != X; l &= l - 1) processSlotRev(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 2));
      for (var l = lo & ~xlo & rlo & vlo; l != X; l &= l - 1) processSlotRev(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 3));
      for (var l = lo & xlo & ~rlo & ~vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 4));
      for (var l = lo & xlo & ~rlo & vlo; l != X; l &= l - 1) processSlot(c, visitor, Slot.packSlotKey(numberOfTrailingZeros(l), 5));
      
      for (var h = hi & ~xhi & ~rhi & vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 1));
      for (var h = hi & ~xhi & ~rhi & ~vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 0));
      for (var h = hi & ~xhi & rhi & ~vhi; h != X; h &= h - 1) processSlotRev(c, visitor, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 2));
      for (var h = hi & ~xhi & rhi & vhi; h != X; h &= h - 1) processSlotRev(c, visitor, Slot.packSlotKey((64 | numberOfTrailingZeros(h)), 3));
      for (var h = hi & xhi & ~rhi & ~vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 4));
      for (var h = hi & xhi & ~rhi & vhi; h != X; h &= h - 1) processSlot(c, visitor, Slot.packSlotKey(64 | numberOfTrailingZeros(h), 5));
   }
   public static Slot[] extractSlots(Clues c, DictEntry[] index, DictEntry[] rev) {
      var slots = new ArrayList<Slot>(c.clueCount());
      forEachSlot(c,(key, lo, hi) -> slots.add(Slot.from(key, lo, hi, Slotinfo.increasing(key) ? index[Slot.length(lo, hi)] : rev[Slot.length(lo, hi)])));
      return slots.toArray(Slot[]::new);
   }
   public static Slotinfo[] slots(Clues mask, Dict d) { return slots(mask, d.index(), d.reversed()); }
   public static Slotinfo[] slots(Clues mask, DictEntry[] index, DictEntry[] rev) {
      var slots = extractSlots(mask, index, rev);
      return scoreSlots(slots);
   }
   public static Slotinfo[] scoreSlots(Slot[] slots) {
      val count    = new byte[SIZE];
      var slotInfo = new Slotinfo[slots.length];
      for (var s : slots) {
         for (var b = s.lo; b != X; b &= b - 1) count[numberOfTrailingZeros(b)]++;
         for (var b = s.hi; b != X; b &= b - 1) count[64 | numberOfTrailingZeros(b)]++;
      }
      for (var i = 0; i < slots.length; i++) {
         var slot = slots[i];
         slotInfo[i] = new Slotinfo(slot.key, slot.lo, slot.hi, slotScore(count, slot.lo, slot.hi), new puzzle.SwedishGenerator.Assign(), slot.entry,
                                    Math.min(slot.entry.words().length, MAX_TRIES_PER_SLOT));
      }
      return slotInfo;
   }
   public static int slotScore(byte[] count, long lo, long hi) {
      var cross = 0;
      for (var b = lo; b != X; b &= b - 1) cross += (count[numberOfTrailingZeros(b)] - 1);
      for (var b = hi; b != X; b &= b - 1) cross += (count[64 | numberOfTrailingZeros(b)] - 1);
      return cross * 10 + Slot.length(lo, hi);
   }
   public static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
   public static int offset(int r, int c)       { return r | (c << 3); }
   
   public long maskFitness(final Clues grid, int clueSize) {
      
      long cHLo     = 0L, cHHi = 0L, cVLo = 0L, cVHi = 0L;
      long cHLo2    = 0L, cHHi2 = 0L, cVLo2 = 0L, cVHi2 = 0L;
      long lo_cl    = grid.lo, hi_cl = grid.hi;
      var  penalty  = (((long) Math.abs(grid.clueCount() - clueSize)) * 16000L);
      var  hasSlots = false;
      
      var numClues = 0;
      for (var bits = lo_cl; bits != X; bits &= bits - 1) {
         var  lsb     = bits & -bits;
         var  clueIdx = numberOfTrailingZeros(lsb);
         int  dir     = grid.getDir(clueIdx);
         var  key     = Slot.packSlotKey(clueIdx, dir);
         long rLo     = PATH_LO[key], rHi = PATH_HI[key];
         long hLo     = rLo & lo_cl, hHi = rHi & hi_cl;
         if (Slotinfo.increasing(key)) {
            if (hLo != X) {
               rLo &= ((1L << numberOfTrailingZeros(hLo)) - 1);
               rHi = 0;
            } else if (hHi != X) rHi &= ((1L << numberOfTrailingZeros(hHi)) - 1);
         } else if (hHi != X) {
            var msb = 63 - numberOfLeadingZeros(hHi);
            rHi &= -(1L << msb << 1);
            rLo = 0;
         } else if (hLo != X) {
            var msb = 63 - numberOfLeadingZeros(hLo);
            rLo &= -(1L << msb << 1);
         }
         
         activeCIdx[numClues] = clueIdx;
         activeSLo[numClues]  = rLo;
         activeSHi[numClues]  = rHi;
         numClues++;
         
         if ((rLo | rHi) != X) {
            hasSlots = true;
            if (Slot.horiz(key)) {
               cHLo2 |= (cHLo & rLo);
               cHHi2 |= (cHHi & rHi);
               cHLo |= rLo;
               cHHi |= rHi;
            } else {
               cVLo2 |= (cVLo & rLo);
               cVHi2 |= (cVHi & rHi);
               cVLo |= rLo;
               cVHi |= rHi;
            }
            if ((bitCount(rLo) + bitCount(rHi)) < MIN_LEN) penalty += 8000;
            var wordLen = bitCount(rLo) + bitCount(rHi);
            if (wordLen > 6) penalty += (wordLen - 6) * 1000L;
         } else penalty += 25000;
      }
      for (var bits = hi_cl; bits != X; bits &= bits - 1) {
         var  lsb     = bits & -bits;
         var  clueIdx = numberOfTrailingZeros(lsb);
         int  dir     = grid.getDir(64 | clueIdx);
         var  key     = Slot.packSlotKey(64 | clueIdx, dir);
         long rLo     = PATH_LO[key], rHi = PATH_HI[key];
         long hLo     = rLo & lo_cl, hHi = rHi & hi_cl;
         if (Slotinfo.increasing(key)) {
            if (hLo != X) {
               rLo &= ((1L << numberOfTrailingZeros(hLo)) - 1);
               rHi = 0;
            } else if (hHi != X) rHi &= ((1L << numberOfTrailingZeros(hHi)) - 1);
         } else if (hHi != X) {
            var msb = 63 - numberOfLeadingZeros(hHi);
            rHi &= -(1L << msb << 1);
            rLo = 0;
         } else if (hLo != X) {
            var msb = 63 - numberOfLeadingZeros(hLo);
            rLo &= -(1L << msb << 1);
         }
         
         activeCIdx[numClues] = 64 | clueIdx;
         activeSLo[numClues]  = rLo;
         activeSHi[numClues]  = rHi;
         numClues++;
         
         if ((rLo | rHi) != X) {
            hasSlots = true;
            if (Slot.horiz(key)) {
               cHLo2 |= (cHLo & rLo);
               cHHi2 |= (cHHi & rHi);
               cHLo |= rLo;
               cHHi |= rHi;
            } else {
               cVLo2 |= (cVLo & rLo);
               cVHi2 |= (cVHi & rHi);
               cVLo |= rLo;
               cVHi |= rHi;
            }
            if ((bitCount(rLo) + bitCount(rHi)) < MIN_LEN) penalty += 8000;
            var wordLen = bitCount(rLo) + bitCount(rHi);
            if (wordLen > 6) penalty += (wordLen - 6) * 1000L;
         } else penalty += 25000;
      }
      
      if (!hasSlots) return 1_000_000_000L;
      
      Arrays.fill(rCount, 0);
      Arrays.fill(cCount, 0);
      for (var i = 0; i < numClues; i++) {
         var idx = activeCIdx[i];
         rCount[idx & 7]++;
         cCount[idx >> 3]++;
      }
      for (var rc : rCount) if (rc < 2) penalty += (2 - rc) * 4000L;
      for (var cc : cCount) if (cc < 2) penalty += (2 - cc) * 4000L;
      
      // Connectiviteitscheck
      for (var i = 0; i < numClues; i++) {
         adjLo[i] = 0;
         adjHi[i] = 0;
      }
      for (var i = 0; i < numClues; i++) {
         for (var j = i + 1; j < numClues; j++) {
            var connected = false;
            // 1. Intersectie
            if (((activeSLo[i] & activeSLo[j]) | (activeSHi[i] & activeSHi[j])) != 0) {
               connected = true;
            }
            
            if (connected) {
               if (j < 64) adjLo[i] |= (1L << j);
               else adjHi[i] |= (1L << (j - 64));
               if (i < 64) adjLo[j] |= (1L << i);
               else adjHi[j] |= (1L << (i - 64));
            }
         }
      }
      
      if (numClues > 0) {
         var  maxReached     = 0;
         long totalReachedLo = 0, totalReachedHi = 0;
         for (var i = 0; i < numClues; i++) {
            if (i < 64) { if ((totalReachedLo & (1L << i)) != 0) continue; } else { if ((totalReachedHi & (1L << (i - 64))) != 0) continue; }
            
            var currentReachedLo = (i < 64) ? (1L << i) : 0;
            var currentReachedHi = (i >= 64) ? (1L << (i - 64)) : 0;
            stack[0] = i;
            var sp    = 1;
            var count = 0;
            while (sp > 0) {
               var cur = stack[--sp];
               count++;
               var nLo = adjLo[cur] & ~currentReachedLo;
               var nHi = adjHi[cur] & ~currentReachedHi;
               while (nLo != 0) {
                  var lsb = nLo & -nLo;
                  var idx = numberOfTrailingZeros(lsb);
                                currentReachedLo |= lsb;
                  stack[sp++] = idx;
                                nLo &= ~lsb;
               }
               while (nHi != 0) {
                  var lsb = nHi & -nHi;
                  var idx = 64 | numberOfTrailingZeros(lsb);
                                currentReachedHi |= lsb;
                  stack[sp++] = idx;
                                nHi &= ~lsb;
               }
            }
            if (count > maxReached) maxReached = count;
            totalReachedLo |= currentReachedLo;
            totalReachedHi |= currentReachedHi;
         }
         if (maxReached < numClues) {
            penalty += (numClues - maxReached) * 4000L;
            penalty += 20000;
         }
      }
      
      for (var bits = ~lo_cl & MASK_LO; bits != X; bits &= bits - 1) {
         var clueIdx = numberOfTrailingZeros(bits);
         var rci     = IT[clueIdx];
         if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400;
         var h = (cHLo & (1L << clueIdx)) != X;
         var v = (cVLo & (1L << clueIdx)) != X;
         if (!h && !v) penalty += 2000;
         else if (h && v) { /* ok */ } else if (((h ? cHLo2 : cVLo2) & (1L << clueIdx)) != X) penalty += 1000;
         else penalty += 1000;
      }
      for (var bits = ~hi_cl & MASK_HI; bits != X; bits &= bits - 1) {
         var clueIdx = numberOfTrailingZeros(bits);
         var rci     = IT[64 | clueIdx];
         if ((4 - rci.nbrCount()) + bitCount(rci.n1() & lo_cl) + bitCount(rci.n2() & hi_cl) >= 3) penalty += 400;
         var h = (cHHi & (1L << clueIdx)) != X;
         var v = (cVHi & (1L << clueIdx)) != X;
         if (!h && !v)
            penalty += 2000;
         else if (h && v) { /* ok */ } else if (((h ? cHHi2 : cVHi2) & (1L << clueIdx)) != X) penalty += 1000;
         else penalty += 1000;
      }
      
      long remLo = lo_cl, remHi = hi_cl;
      while ((remLo | remHi) != X) {
         long compLo = 0, compHi = 0;
         if (remLo != X) compLo = lowestOneBit(remLo);
         else compHi = lowestOneBit(remHi);
         long lastLo, lastHi;
         do {
            lastLo = compLo;
            lastHi = compHi;
            long expandedLo = 0, expandedHi = 0;
            for (var bits = compLo; bits != X; bits &= bits - 1) {
               var idx = numberOfTrailingZeros(bits);
               expandedLo |= NBR_LO[idx];
               expandedHi |= NBR_HI[idx];
            }
            for (var bits = compHi; bits != X; bits &= bits - 1) {
               var idx = 64 | numberOfTrailingZeros(bits);
               expandedLo |= NBR_LO[idx];
               expandedHi |= NBR_HI[idx];
            }
            compLo |= expandedLo & lo_cl;
            compHi |= expandedHi & hi_cl;
         } while (compLo != lastLo || compHi != lastHi);
         var s = bitCount(compLo) + bitCount(compHi);
         if (s >= 2) penalty += (long) (s - 1) * 520;
         remLo &= ~compLo;
         remHi &= ~compHi;
      }
      
      return penalty;
   }
   public static boolean hasRoomForClue(Clues c,int key) {
      if (Slotinfo.increasing(key)) if (!validSlot(c.lo, c.hi, key)) return false;
      return validSlotRev(c.lo, c.hi, key);
   }
   public Clues randomMask(final int clueSize) {
      var g = Clues.createEmpty();
      for (int placed = 0, guard = 0, ri; placed < clueSize && guard < 4000; guard++) {
         
         ri = rng.randint0_SIZE();
         if (isLo(ri)) {
            if (g.isClueLo(ri)) continue;
            var d_idx = rng.randomClueDir();
            var key   = Slot.packSlotKey(ri, d_idx);
            if (hasRoomForClue(g,key)) {
               g.setClueLo(1L << ri, d_idx);
               if (isValid(g)) placed++;
               else g.clearClueLo(~(1L << ri));
            }
         } else {
            if (g.isClueHi(ri)) continue;
            var d_idx = rng.randomClueDir();
            var key   = Slot.packSlotKey(ri, d_idx);
            if (hasRoomForClue(g,key)) {
               g.setClueHi(1L << (ri & 63), d_idx);
               if (isValid(g)) placed++;
               else g.clearClueHi(~(1L << (ri & 63)));
            }
         }
         
      }
      return g;
   }
   
   public Clues mutate(Clues c) {
      var bytes = MUTATE_RI[rng.randint0_SIZE()];
      for (int k = 0, ri; k < 6; k++) {
         ri = bytes[rng.randint0_624()];
         if (c.notClue(ri)) { // ADD
            var d   = rng.randomClueDir();
            var key = Slot.packSlotKey(ri, d);
            if (hasRoomForClue(c,key)) {
               if (isLo(ri)) {
                  c.setClueLo(1L << ri, d);
                  if (!isValid(c)) c.clearClueLo(~(1L << ri));
                  else continue;
               } else {
                  c.setClueHi(1L << (ri & 63), d);
                  if (!isValid(c)) c.clearClueHi(~(1L << (ri & 63)));
                  else continue;
               }
            }
         } else { // HAS CLUE
            var op = rng.randomClueDir();
            if (op < 2) { // REMOVE
               var oldD = c.getDir(ri);
               if (isLo(ri)) {
                  c.clearClueLo(~(1L << ri));
                  if (!isValid(c)) c.setClueLo(1L << ri, oldD);
                  else continue;
               } else {
                  c.clearClueHi(~(1L << (ri & 63)));
                  if (!isValid(c)) c.setClueHi(1L << (ri & 63), oldD);
                  else continue;
               }
            }
            if (op < 4) { // CHANGE DIRECTION
               var d   = rng.randomClueDir();
               var key = Slot.packSlotKey(ri, d);
               if (hasRoomForClue(c,key)) {
                  var oldD = c.getDir(ri);
                  if (isLo(ri)) {
                     c.setClueLo(1L << ri, d);
                     if (!isValid(c)) c.setClueLo(1L << ri, oldD);
                     else continue;
                  } else {
                     c.setClueHi(1L << (ri & 63), d);
                     if (!isValid(c)) c.setClueHi(1L << (ri & 63), oldD);
                     else continue;
                  }
               }
            }  // MOVE
            var nri = bytes[rng.randint0_624()];
            if (c.notClue(nri)) {
               var d    = c.getDir(ri);
               var nkey = Slot.packSlotKey(nri, d);
               if (hasRoomForClue(c,nkey)) {
                  if (isLo(ri)) c.clearClueLo(~(1L << ri));
                  else c.clearClueHi(~(1L << (ri & 63)));
                  if (isLo(nri)) c.setClueLo(1L << nri, d);
                  else c.setClueHi(1L << (nri & 63), d);
                  if (!isValid(c)) {
                     if (isLo(nri)) c.clearClueLo(~(1L << nri));
                     else c.clearClueHi(~(1L << (nri & 63)));
                     if (isLo(ri)) c.setClueLo(1L << ri, d);
                     else c.setClueHi(1L << (ri & 63), d);
                  } else continue;
               }
            }
         }
      }
      return c;
   }
   
   public Clues crossover(Clues a, Clues other) {
      
      var theta = rng.nextFloat() * Math.PI;
      var nc    = Math.cos(theta);
      var nr    = Math.sin(theta);
      
      long maskLo = 0, maskHi = 0;
      for (var rci : IT)
         if ((rci.cross_r()) * nc + (rci.cross_c()) * nr < 0) {
            var i = rci.i();
            if ((i & 64) == 0) maskLo |= (1L << i);
            else maskHi |= (1L << (i - 64));
         }
      var c = new Clues(
              (a.lo & ~maskLo) | (other.lo & maskLo),
              (a.hi & ~maskHi) | (other.hi & maskHi),
              (a.vlo & ~maskLo) | (other.vlo & maskLo),
              (a.vhi & ~maskHi) | (other.vhi & maskHi),
              (a.rlo & ~maskLo) | (other.rlo & maskLo),
              (a.rhi & ~maskHi) | (other.rhi & maskHi),
              (a.xlo & ~maskLo) | (other.xlo & maskLo),
              (a.xhi & ~maskHi) | (other.xhi & maskHi));
      cleanup(c);
      return c;
   }
   
   public Clues hillclimb(Clues start, int clue_size, int limit) {
      var best  = start;
      var bestF = maskFitness(best, clue_size);
      var fails = 0;
      
      while (fails < limit) {
         cache.from(best);
         var cand = mutate(best);
         var f    = maskFitness(cand, clue_size);
         if (f < bestF) {
            best  = cand;
            bestF = f;
            fails = 0;
         } else {
            best.from(cache);
            fails++;
         }
      }
      return best;
   }
   
   public Clues generateMask(int clueSize, int popSize, int gens, int offspring) {
      class GridAndFit {
         
         Clues grid;
         long  fite = -1;
         GridAndFit(Clues grid) { this.grid = grid; }
         long fit() {
            if (fite == -1) this.fite = maskFitness(grid, clueSize);
            return this.fite;
         }
      }
      if (VERBOSE) System.out.println("generateMask init pop: " + popSize + " clueSize: " + clueSize);
      var pop = new GridAndFit[popSize];
      for (var i = 0; i < popSize; i++) {
         if (Thread.currentThread().isInterrupted()) return null;
         pop[i] = new GridAndFit(hillclimb(randomMask(clueSize), clueSize, 180));
      }
      
      for (var gen = 0; gen < gens; gen++) {
         if (Thread.currentThread().isInterrupted()) break;
         var children   = new GridAndFit[offspring];
         var childCount = 0;
         for (var k = 0; k < offspring; k++) {
            if (Thread.currentThread().isInterrupted()) break;
            var p1    = rng.rand(pop);
            var p2    = rng.rand(pop);
            var child = crossover(p1.grid, p2.grid);
            children[k] = new GridAndFit(hillclimb(child, clueSize, 70));
            childCount++;
         }
         
         var combined = new GridAndFit[pop.length + childCount];
         System.arraycopy(pop, 0, combined, 0, pop.length);
         System.arraycopy(children, 0, combined, pop.length, childCount);
         Arrays.sort(combined, Comparator.comparingLong(GridAndFit::fit));
         
         var next      = new GridAndFit[popSize];
         var nextCount = 0;
         for (var cand : combined) {
            if (nextCount >= popSize) break;
            var unique = true;
            for (var i = 0; i < nextCount; i++) {
               if (similarity(cand.grid, next[i].grid) > 0.92) {
                  unique = false;
                  break;
               }
            }
            if (unique) next[nextCount++] = cand;
         }
         
         if (nextCount < popSize) {
            for (var cand : combined) {
               if (nextCount >= popSize) break;
               var alreadyIn = false;
               for (var i = 0; i < nextCount; i++) {
                  if (cand == next[i]) {
                     alreadyIn = true;
                     break;
                  }
               }
               if (!alreadyIn) next[nextCount++] = cand;
            }
         }
         pop = nextCount == popSize ? next : Arrays.copyOf(next, nextCount);
         
         if (VERBOSE && (gen & 15) == 15) System.out.println("  gen " + gen + "/" + gens + " bestFitness=" + pop[0].fit());
      }
      if (pop.length == 0) return null;
      var best = pop[0];
      for (var i = 1; i < pop.length; i++) {
         var x = pop[i];
         if (x.fit() < best.fit()) best = x;
      }
      return best.grid;
   }
   //@formatter:off
   @FunctionalInterface public interface SlotVisitor { void visit(int key, long lo, long hi); }
   
   public record Slot(int key, long lo, long hi, DictEntry entry) {
      
      static final int BIT_FOR_DIR = 3;
      public static Slot from(int key, long lo, long hi, DictEntry entry) { return new Slot(key, lo, hi, entry); }
      public static int length(long lo, long hi)                          { return bitCount(lo) + bitCount(hi); }
      public static int clueIndex(int key)                                { return key >>> BIT_FOR_DIR; }
      public static int dir(int key)                                      { return key & 7; }
      public static boolean horiz(int d)                                  { return (d == 1) || (d == 3); }
      public static int packSlotKey(int idx, int d)                       { return (idx << BIT_FOR_DIR) | d; }
   }
}
