package puzzle;

import com.google.gson.Gson;
import lombok.Getter;
import lombok.val;
import precomp.Neighbors9x8;
import precomp.Neighbors9x8.nbrs_16;
import precomp.Neighbors9x8.nbrs_8;
import precomp.Neighbors9x8.rci;
import puzzle.Export.Bit;
import puzzle.Export.Bit1029;
import puzzle.Export.Gridded;
import puzzle.Export.Strings;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.IntConsumer;
import java.util.function.Supplier;

/**
 * SwedishGenerator.java
 *
 * Usage:
 *   javac SwedishGenerator.java
 *   java SwedishGenerator [--seed N] [--pop N] [--gens N] [--tries N] [--words word-list.txt]
 */
@SuppressWarnings("ALL")
public record SwedishGenerator(Rng rng) {
   
   record CandidateInfo(int[] indices, int count) { }
   
   //@formatter:off
   @FunctionalInterface interface SlotVisitor { void visit(int key, long packedPos, int len); }
   //@formatter:on
   static final int    LOG_EVERY_MS             = 200;
   static final int    BAR_LEN                  = 22;
   static final int    C                        = Config.PUZZLE_COLS;
   static final double CROSS_R                  = (C - 1) / 2.0;
   static final int    R                        = Config.PUZZLE_ROWS;
   static final double CROSS_C                  = (R - 1) / 2.0;
   static final int    SIZE                     = C * R;// ~18
   static final double SIZED                    = (double) SIZE;// ~18
   static final int    TARGET_CLUES             = SIZE >> 2;
   static final int    MAX_WORD_LENGTH          = C <= R ? C : R;
   static final int    MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1;
   static final int    MIN_LEN                  = Config.MIN_LEN;
   static final int    CLUE_SIZE                = Config.CLUE_SIZE;
   static final int    SIMPLICITY_DEFAULT_SCORE = 2;
   static final int    MAX_TRIES_PER_SLOT       = Config.MAX_TRIES_PER_SLOT;
   static final char   C_DASH                   = '\0';
   static final byte   _1                       = 49, _9 = 57, A = 65, Z = 90, DASH = (byte) C_DASH;
   static final ThreadLocal<Context> CTX = ThreadLocal.withInitial(Context::new);
   static boolean isLetter(byte b)       { return (b & 64) != 0; }
   static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
   record Pick(Slot slot, CandidateInfo info, boolean done) { }
   
   // Directions for '1'..'6'
   static final nbrs_16[] OFFSETS       = Neighbors9x8.OFFSETS;
   static final nbrs_8[]  nbrs8         = Neighbors9x8.nbrs8;
   static final nbrs_8[]  nbrs4         = Neighbors9x8.nbrs4;
   static final rci[]     IT            = Neighbors9x8.IT;
   static final long[]    INBR8_PACKEDT = Neighbors9x8.NBR8_PACKED;
   
   static final Pick PICK_DONE     = new Pick(null, null, true);
   static final Pick PICK_NOT_DONE = new Pick(null, null, false);
   
   public static final class FillStats {
      
      public long   nodes;
      public long   backtracks;
      public double seconds;
      public int    lastMRV;
      public double simplicity;
   }
   
   public static record FillResult(boolean ok,
                                   Gridded grid,
                                   HashMap<Integer, Lemma> clueMap,
                                   FillStats stats) {
      
      public FillResult {
         if (ok) {
            clueMap.forEach((k, v) -> stats.simplicity += v.simpel);
            stats.simplicity = clueMap.isEmpty() ? 0 : stats.simplicity / clueMap.size();
         }
      }
   }
   
   static record Context(int[] covH,
                         int[] covV,
                         int[] cellCount,
                         int[] stack,
                         Bit seen,
                         byte[] pattern,
                         IntList[] intListBuffer,
                         int[] undo,
                         int[] inter1,
                         int[] inter2) {
      
      public Context() {
         this(new int[SIZE], new int[SIZE], new int[SIZE], new int[SIZE], new Bit(), new byte[MAX_WORD_LENGTH], new IntList[MAX_WORD_LENGTH],
              new int[2048], new int[160000], new int[160000]);
      }
      void setPatter(byte[] chars) { System.arraycopy(chars, 0, this.pattern, 0, chars.length); }
   }
   
   static final class Rng {
      
      @Getter private int x;
      Rng(int seed) {
         var s = seed;
         if (s == 0) s = 1;
         this.x = s;
      }
      int nextU32() {
         var y = x;
         y ^= (y << 13);
         y ^= (y >>> 17);
         y ^= (y << 5);
         x = y;
         return y;
      }
      byte randbyte(int min, int max) {
         var u     = (nextU32() & 0xFFFFFFFFL);
         var range = (long) max - (long) min + 1L;
         return (byte) (min + (u % range));
      }
      int randint(int min, int max) {
         var u     = (nextU32() & 0xFFFFFFFFL);
         var range = (long) max - (long) min + 1L;
         return (int) (min + (u % range));
      }
      double nextFloat() { return (nextU32() & 0xFFFFFFFFL) / 4294967295.0; }
   }
   
   record Grid(byte[] g, long[] bo) {
      
      public Grid(byte[] g)            { this(g, new long[2]); }
      static Grid createEmpty()        { return new Grid(new byte[SIZE], new long[2]); }
      int digitAt(int r, int c)        { return g[offset(r, c)] - 48; }
      int digitAt(int index)           { return g[index] - 48; }
      public static int r(int offset)  { return offset & 7; }
      public static int c(int offset)  { return offset >>> 3; }
      static int offset(int r, int c)  { return r | (c << 3); }
      Grid deepCopyGrid()              { return new Grid(g.clone(), bo.clone()); }
      public byte byteAt(int r, int c) { return g[offset(r, c)]; }
      public byte byteAt(int pos)      { return g[pos]; }
      void setByteAt(int idx, byte ch) { g[idx] = ch; }
      void setAt(int idx, byte ch) {
         if (isDigit(ch)) setClue(idx, ch);
         else g[idx] = ch;
      }
      void setClue(int idx, byte ch) {
         g[idx] = ch;
         if (idx < 64) bo[0] |= (1L << idx);
         else bo[1] |= (1L << (idx & 63));
      }
      void clear(int idx) { g[idx] = DASH; }
      void clearClue(int idx) {
         g[idx] = DASH;
         if (idx < 64) bo[0] &= ~(1L << idx);
         else bo[1] &= ~(1L << (idx & 63));
      }
      static boolean isDigit(byte b)  { return (b & 48) == 48; }
      boolean isDigitAt(int r, int c) { return isDigit(g[offset(r, c)]); }
      boolean isDigitAt(int index)    { return isDigit(g[index]); }
      boolean isClue(int index) {
         if (index < 64) {
            return (bo[0] >> index & 1L) != 0;
         }
         return (bo[1] >> (index & 63) & 1L) != 0;
      }
      boolean clueless(int idx) {
         if (idx < 64) {
            val test = (1L << idx);
            if ((test & bo[0]) != 0L) {
               g[idx] = DASH;
               bo[0] &= ~test;
               return true;
            }
            return false;
         } else {
            val test = (1L << (idx & 63));
            if ((test & bo[1]) != 0L) {
               g[idx] = DASH;
               bo[1] &= ~test;
               return true;
            }
            return false;
         }
      }
      static boolean isLetter(byte b)          { return (b & 64) != 0; }
      public boolean isLetterSet(int r, int c) { return isLetter(g[offset(r, c)]); }
      public boolean isLetterSet(int idx)      { return isLetter(g[idx]); }
      static boolean notDigit(byte b)          { return (b & 48) != 48; }
      public boolean isLetterAt(int r, int c)  { return notDigit(g[offset(r, c)]); }
      public boolean isLetterAt(int index)     { return notDigit(g[index]); }
      
      public double similarity(Grid b) {
         var same = 0;
         for (int i = 0; i < SIZE; i++) if (g[i] == b.g[i]) same++;
         return same / SIZED;
      }
      int clueCount() { return Long.bitCount(bo[0]) + Long.bitCount(bo[1]); }
      
      void forEachSetBit71(IntConsumer consumer) {
         for (var lo = bo[0]; lo != 0; lo &= lo - 1) consumer.accept(Long.numberOfTrailingZeros(lo));
         for (var hi = bo[1]; hi != 0; hi &= hi - 1) consumer.accept(64 + Long.numberOfTrailingZeros(hi));
      }
      void forEachSlot(SlotVisitor visitor) {
         for (var lo = bo[0]; lo != 0; lo &= lo - 1) processSlot(this, visitor, Long.numberOfTrailingZeros(lo));
         for (var hi = bo[1]; hi != 0; hi &= hi - 1) processSlot(this, visitor, 64 + Long.numberOfTrailingZeros(hi));
      }
   }
   
   static final class IntList {
      
      int[] a = new int[8];
      int   n = 0;
      void add(int v) {
         if (n >= a.length) a = Arrays.copyOf(a, a.length * 2);
         a[n++] = v;
      }
      int size()   { return n; }
      int[] data() { return a; }
   }
   
   static record DictEntry(ArrayList<Lemma> words, IntList[][] pos) {
      
      public DictEntry(int L) {
         this(new ArrayList<>(), new IntList[L][26]);
         for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntList();
      }
   }
   
   public static record Lemma(int index, byte[] word, int simpel, String[] clue) {
      
      static int LEMMA_COUNTER = 0;
      public Lemma(int index, String word, int simpel, String[] clu) { this(index, word.getBytes(StandardCharsets.US_ASCII), simpel, clu); }
      public Lemma(String word, int simpel, String clue)             { this(LEMMA_COUNTER++, word, simpel, new String[]{ clue }); }
      byte byteAt(int idx)                                           { return word[idx]; }
      @Override public int hashCode()                                { return index; }
      @Override public boolean equals(Object o)                      { return (o == this) || (o instanceof Lemma l && l.index == index); }
   }
   
   public static record Dict(
           DictEntry[] index,
           int length) {
      
      static final Gson GSON = new Gson();
      public Dict(Lemma[] wordz) {
         var index = new DictEntry[MAX_WORD_LENGTH_PLUS_ONE];
         Arrays.setAll(index, i -> new DictEntry(i));
         for (var lemma : wordz) {
            var L = lemma.word.length;
            
            var entry = index[L];
            var idx   = entry.words.size();
            entry.words.add(lemma);
            
            for (var i = 0; i < L; i++) {
               var letter = lemma.byteAt(i) - 'A';
               if (letter < 0 || letter >= 26) throw new RuntimeException("Illegal letter: " + letter + " in word " + lemma);
               entry.pos[i][letter].add(idx);
            }
         }
         for (int i = MIN_LEN; i < index.length; i++) {
            var len = index[i].words.size();
            if (len <= 0) {
               throw new RuntimeException("No words for length " + i);
            }
         }
         this(index, Arrays.stream(index).mapToInt(i -> i.words.size()).sum());
      }
      static Dict loadDict(String wordsPath) {
         String raw;
         try {
            raw = Files.readString(Path.of(wordsPath), StandardCharsets.UTF_8);
         } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Failed to load dictionary from " + wordsPath, e);
         }
         
         var map   = new ArrayList<Lemma>();
         var first = true;
         for (var line : raw.split("\\R")) {
            if (line.isBlank()) {
               System.err.println("Empty line: " + line);
               continue;
            }
            var parts = line.split(",", 5);
            var id    = Integer.parseInt(parts[0].trim());
            var word  = parts[1].trim();
            if (first && word.equalsIgnoreCase("WOORD")) {
               first = false;
               continue;
            }
            first = false;
            var s = word.toUpperCase(Locale.ROOT);
            if (!s.matches("^[A-Z]{2,8}$")) {
               System.err.println("Invalid word: " + line);
               continue;
            }
            
            // CSV has level 1-10. llmScores use 10-level.
            int score = Integer.parseInt(parts[2].trim());
            if (score < 1) {
               if (Main.VERBOSE) System.err.println("Word too complex: " + line);
               continue;
            }
            int simpel  = Integer.parseInt(parts[3].trim());
            var rawClue = parts[4].trim();
            if (rawClue.startsWith("\"") && rawClue.endsWith("\"")) {
               rawClue = rawClue.substring(1, rawClue.length() - 1).replace("\"\"", "\"");
            }
            map.add(new Lemma(id, s, simpel, GSON.fromJson(rawClue, String[].class)));
         }
         
         return new Dict(map.toArray(Lemma[]::new));
      }
   }
   
   static int intersectSorted(int[] a, int aLen, int[] b, int bLen, int[] out) {
      int i = 0, j = 0, k = 0;
      while (i < aLen && j < bLen) {
         int x = a[i];
         int y = b[j];
         if (x == y) {
            out[k++] = x;
            i++;
            j++;
         } else if (x < y) i++;
         else j++;
      }
      return k;
   }
   
   static record Slot(int key, long packedPos) {
      
      static Slot from(int key, long packedPos, int len) { return new Slot(key, packedPos | ((long) len << 56)); }
      void undoPlace(Grid grid, int mask)                { for (int i = 0, len = len(); i < len; i++) if ((mask & (1L << i)) != 0) grid.clear(pos(i)); }
      public int len()                                   { return (int) (packedPos >>> 56); }
      public int clueR()                                 { return Grid.r((key >>> 4)); }
      public int clueIndex()                             { return key >>> 4; }
      public int clueC()                                 { return Grid.c((key >>> 4)); }
      public int dir()                                   { return key & 15; }
      public boolean horiz()                             { return horiz(key); }
      public int pos(int i)                              { return offset(packedPos, i); }
      public static boolean horiz(int key)               { return ((key & 15) & 1) == 0; }
      public static int offset(long packedPos, int i)    { return (int) ((packedPos >> (i * 7)) & 127); }
   }
   
   private static void processSlot(Grid grid, SlotVisitor visitor, int idx) {
      var  d         = grid.digitAt(idx);
      var  packed    = OFFSETS[d].path()[idx];
      long packedPos = 0;
      int  k         = 0;
      for (int n = (int) (packed >>> 56), iidx; k < n && k < MAX_WORD_LENGTH; k++) {
         iidx = (int) ((packed >>> (k * 7)) & 0x7F);
         if (grid.isClue(iidx)) break;
         packedPos |= (long) iidx << (k * 7);
      }
      if (k > 0) {
         visitor.visit((idx << 4) | d, packedPos, k);
      }
   }
   
   static ArrayList<Slot> extractSlots(Grid grid) {
      var slots = new ArrayList<Slot>(32);
      grid.forEachSlot((key, packedPos, len) -> slots.add(Slot.from(key, packedPos, len)));
      return slots;
   }
   boolean hasRoomForClue(Grid grid, long packed) {
      for (int n = (int) (packed >>> 56), k = 0; k < n && k < MAX_WORD_LENGTH; ) {
         if (!grid.isLetterAt((int) ((packed >>> (k * 7)) & 0x7F))) break;
         if (++k >= MIN_LEN) return true;
      }
      return false;
   }
   
   long maskFitness(Grid grid) {
      final long[] penalty = { 0 };
      
      var clueCount = grid.clueCount();
      penalty[0] += ((long) Math.abs(clueCount - TARGET_CLUES)) << 3;
      
      var ctx  = CTX.get();
      var covH = ctx.covH;
      var covV = ctx.covV;
      Arrays.fill(covH, 0, SIZE, 0);
      Arrays.fill(covV, 0, SIZE, 0);
      
      boolean hasSlots = false;
      long    lo_cl    = grid.bo[0], hi_cl = grid.bo[1];
      while (lo_cl != 0) {
         int clueIdx = Long.numberOfTrailingZeros(lo_cl);
         lo_cl &= (lo_cl - 1);
         var  d      = grid.digitAt(clueIdx);
         var  nbrs16 = OFFSETS[d];
         long packed = nbrs16.path()[clueIdx];
         int  n      = (int) (packed >>> 56), k, idx;
         var  horiz  = Slot.horiz(d) ? covH : covV;
         for (k = 0; k < n && k < MAX_WORD_LENGTH; k++) {
            idx = (int) ((packed >>> (k * 7)) & 0x7F);
            if (grid.isClue(idx)) break;
            horiz[idx] += 1;
         }
         if (k == 0) continue;
         hasSlots = true;
         if (k < MIN_LEN) penalty[0] += 8000;
      }
      while (hi_cl != 0) {
         int clueIdx = 64 + Long.numberOfTrailingZeros(hi_cl);
         hi_cl &= (hi_cl - 1);
         var  d      = grid.digitAt(clueIdx);
         var  nbrs16 = OFFSETS[d];
         long packed = nbrs16.path()[clueIdx];
         int  n      = (int) (packed >>> 56), k, idx;
         var  horiz  = Slot.horiz(d) ? covH : covV;
         for (k = 0; k < n && k < MAX_WORD_LENGTH; k++) {
            idx = (int) ((packed >>> (k * 7)) & 0x7F);
            if (grid.isClue(idx)) break;
            horiz[idx] += 1;
         }
         if (k == 0) continue;
         hasSlots = true;
         if (k < MIN_LEN) penalty[0] += 8000;
      }
      
      if (!hasSlots) return 1_000_000_000L;
      
      // clue clustering (8-connected)
      var seen = ctx.seen;
      seen.clear();
      var stack = ctx.stack;
      
      grid.forEachSetBit71(clueIdx -> {
         if (seen.get(clueIdx)) return;
         var sp = 0;
         stack[sp++] = clueIdx;
         seen.set(clueIdx);
         var size = 0;
         
         while (sp > 0) {
            var p  = stack[--sp];
            int rr = Grid.c(p), cc = Grid.r(p);
            size++;
            long packed = Neighbors9x8.NBR8_PACKED[Grid.offset(rr, cc)];
            int  n      = (int) (packed >>> 56);
            for (int k = 0; k < n; k++) {
               int nidx = (int)((packed >>> (k * 7)) & 0x7F);
               
               if (seen.get(nidx) || grid.isLetterAt(nidx)) continue;
               seen.set(nidx);
               stack[sp++] = nidx;
            }
           /* for (var d : nbrs8) {
               var nr = rr + d.r();
               var nc = cc + d.c();
               if (nr < 0 || nr >= R || nc < 0 || nc >= C) continue;
               var nidx = Grid.offset(nr, nc);
               if (seen.get(nidx) || grid.isLetterAt(nidx)) continue;
               seen.set(nidx);
               stack[sp++] = nidx;
            } */
         }
         
         if (size >= 2) penalty[0] += (size - 1L) * 120L;
      });
      // dead-end-ish letter cell (3+ walls)
      int walls, wc, wr;
      /*for (var rci : IT) {
         if (grid.isDigitAt(rci.i())) continue;
         walls = 0;
         for (var d : nbrs4) {
            wr = rci.r() + d.r();
            wc = rci.c() + d.c();
            if (wr < 0 || wr >= R || wc < 0 || wc >= C || grid.isDigitAt(wr, wc)) walls++;
         }
         if (walls >= 3) penalty[0] += 400;
      }*/
      int h, v;
      for (var rci : IT) {
         if (grid.isDigitAt(rci.i())) continue;
         if ((4 - rci.nbrCount()) + Long.bitCount(rci.n1() & grid.bo[0]) + Long.bitCount(rci.n2() & grid.bo[1]) >= 3) penalty[0] += 400;
         h = covH[rci.i()];
         v = covV[rci.i()];
         if (h == 0 && v == 0) penalty[0] += 1500;
         else if (h > 0 && v > 0) { /* ok */ } else if (h + v == 1) penalty[0] += 200;
         else penalty[0] += 600;
      }
      return penalty[0];
   }
   
   Grid randomMask() {
      var g = Grid.createEmpty();
      for (int placed = 0, guard = 0, idx; placed < TARGET_CLUES && guard < 4000; guard++) {
         idx = Grid.offset(rng.randint(0, R - 1),
                           rng.randint(0, C - 1));
         if (g.isClue(idx)) continue;
         var d = OFFSETS[rng.randbyte(1, 4)];
         
         if (hasRoomForClue(g, d.path()[idx])) {
            g.setClue(idx, d.dbyte());
            placed++;
         }
      }
      return g;
   }
   Grid mutate(Grid grid) {
      var g  = grid.deepCopyGrid();
      var cx = rng.randint(0, R - 1);
      var cy = rng.randint(0, C - 1);
      
      var steps = 4;
      for (var k = 0; k < steps; k++) {
         var ri = Grid.offset(
                 clamp(cx + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, R - 1),
                 clamp(cy + (rng.randint(-2, 2) + rng.randint(-2, 2)), 0, C - 1));
         
         if (!g.clueless(ri)) {
            var d = OFFSETS[rng.randint(1, 4)];
            if (hasRoomForClue(g, d.path()[ri])) g.setClue(ri, d.dbyte());
         }
      }
      return g;
   }
   Grid crossover(Grid a, Grid b) {
      var out   = Grid.createEmpty();
      var theta = rng.nextFloat() * Math.PI;
      var nc    = Math.cos(theta);
      var nr    = Math.sin(theta);
      
      for (var rci : IT) out.setAt(rci.i(), ((rci.r() - CROSS_C) * nc + (rci.c() - CROSS_R) * nr >= 0) ? a.byteAt(rci.i()) : b.byteAt(rci.i()));
      for (var rci : IT) if (out.isClue(rci.i()) && !hasRoomForClue(out, OFFSETS[out.digitAt(rci.i())].path()[rci.i()])) out.clearClue(rci.i());
      return out;
   }
   
   Grid hillclimb(Grid start, int limit) {
      var best  = start;
      var bestF = maskFitness(best);
      var fails = 0;
      
      while (fails < limit) {
         var cand = mutate(best);
         var f    = maskFitness(cand);
         if (f < bestF) {
            best  = cand;
            bestF = f;
            fails = 0;
         } else {
            fails++;
         }
      }
      return best;
   }
   
   public Grid generateMask(int popSize, int gens) {
      class GridAndFit {
         
         Grid grid;
         Long fite;
         GridAndFit(Grid grid) { this.grid = grid; }
         long fit() {
            if (fite == null) this.fite = maskFitness(grid);
            return this.fite;
         }
      }
      if (Main.VERBOSE) System.out.println("generateMask init pop: " + popSize);
      var pop = new ArrayList<GridAndFit>();
      for (var i = 0; i < popSize; i++) {
         pop.add(new GridAndFit(hillclimb(randomMask(), 180)));
      }
      
      for (var gen = 0; gen < gens; gen++) {
         if (Thread.currentThread().isInterrupted()) break;
         var children = new ArrayList<GridAndFit>();
         var pairs    = Math.max(popSize, (int) Math.floor(popSize * 1.5));
         
         for (var k = 0; k < pairs; k++) {
            var p1    = pop.get(rng.randint(0, pop.size() - 1));
            var p2    = pop.get(rng.randint(0, pop.size() - 1));
            var child = crossover(p1.grid, p2.grid);
            children.add(new GridAndFit(hillclimb(child, 70)));
         }
         
         pop.addAll(children);
         pop.sort(Comparator.comparingLong(GridAndFit::fit));
         
         var next = new ArrayList<GridAndFit>();
         for (var cand : pop) {
            if (next.size() >= popSize) break;
            var ok = true;
            for (var kept : next) {
               if (cand.grid.similarity(kept.grid) > 0.92) {
                  ok = false;
                  break;
               }
            }
            if (ok) next.add(cand);
         }
         pop = next;
         
         if (Main.VERBOSE && gen % 10 == 0) {
            var bestF = pop.get(0).fit();
            System.out.println("  gen " + gen + "/" + gens + " bestFitness=" + bestF);
         }
      }
      
      pop.sort(Comparator.comparingLong(GridAndFit::fit));
      return pop.get(0).grid;
   }
   static void patternForSlot(Grid grid, Slot s, byte[] pat) {
      for (int i = 0, len = s.len(); i < len; i++) {
         var ch = grid.byteAt(s.pos(i));
         pat[i] = isLetter(ch) ? ch : DASH;
      }
   }
   
   static int slotScore(int[] count, Slot s) {
      int cross = 0, len = s.len();
      for (int i = 0; i < len; i++) cross += (count[s.pos(i)] - 1);
      return cross * 10 + len;
   }
   static boolean placeWord(Grid grid, Slot s, Lemma w, int[] undoBuffer, int offset) {
      int  mask = 0;
      byte cur, ch;
      for (int i = 0, leng = s.len(), idx; i < leng; i++) {
         idx = s.pos(i);
         cur = grid.byteAt(idx);
         ch  = w.byteAt(i);
         if (cur == DASH) {
            mask |= (1 << i);
            grid.setByteAt(idx, ch);
         } else if (cur != ch) {
            for (var j = 0; j < i; j++) {
               if ((mask & (1 << j)) != 0) {
                  grid.clear(s.pos(j));
               }
            }
            return false;
         }
      }
      undoBuffer[offset] = mask;
      return true;
   }
   static CandidateInfo candidateInfoForPattern(Context ctx, DictEntry entry, int len) {
      var pattern    = ctx.pattern;
      var listBuffer = ctx.intListBuffer;
      var listCount  = 0;
      for (var i = 0; i < len; i++) {
         var ch = pattern[i];
         if (isLetter(ch)) {
            listBuffer[listCount++] = entry.pos[i][ch - 'A'];
         }
      }
      
      if (listCount == 0) {
         return new CandidateInfo(null, entry.words.size());
      }
      
      // Sort constraints by size to optimize intersection
      for (int i = 0; i < listCount - 1; i++) {
         for (int j = i + 1; j < listCount; j++) {
            if (listBuffer[j].size() < listBuffer[i].size()) {
               var tmp = listBuffer[i];
               listBuffer[i] = listBuffer[j];
               listBuffer[j] = tmp;
            }
         }
      }
      
      var cur    = listBuffer[0].data();
      var curLen = listBuffer[0].size();
      if (listCount == 1) return new CandidateInfo(cur, curLen);
      
      int[] b1  = ctx.inter1;
      int[] b2  = ctx.inter2;
      int[] in  = cur;
      int[] out = b1;
      
      for (var k = 1; k < listCount; k++) {
         var nxt = listBuffer[k];
         curLen = intersectSorted(in, curLen, nxt.data(), nxt.size(), out);
         in     = out;
         out    = (out == b1) ? b2 : b1;
         if (curLen == 0) break;
      }
      
      return new CandidateInfo(in, curLen);
   }
   public FillResult fillMask(Grid mask, DictEntry[] dictIndex,
                              int timeLimitMs) {
      val multiThreaded = Thread.currentThread().getName().contains("pool");
      val grid          = mask.deepCopyGrid();
      val used          = new Bit1029();
      val assigned      = new HashMap<Integer, Lemma>();
      val ctx           = CTX.get();
      val count         = ctx.cellCount;
      Arrays.fill(count, 0, SIZE, 0);
      
      val slots = extractSlots(grid);
      for (var s : slots) for (int i = 0, len = s.len(); i < len; i++) count[s.pos(i)]++;
      
      val t0    = System.currentTimeMillis();
      val stats = new FillStats();
      val TOTAL = slots.size();
      
      class Solver {
         
         long lastLog = t0;
         void renderProgress() {
            if (!Main.VERBOSE || multiThreaded) return;
            var now = System.currentTimeMillis();
            if ((now - lastLog) < LOG_EVERY_MS) return;
            lastLog = (now);
            
            var done    = assigned.size();
            var pct     = (TOTAL == 0) ? 100 : (int) Math.floor((done / (double) TOTAL) * 100);
            var filled  = Math.min(BAR_LEN, (int) Math.floor((pct / 100.0) * BAR_LEN));
            var bar     = "[" + "#".repeat(filled) + "-".repeat(BAR_LEN - filled) + "]";
            var elapsed = String.format(Locale.ROOT, "%.1fs", (now - t0) / 1000.0);
            
            var msg = String.format(
                    Locale.ROOT,
                    "%s %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %s",
                    bar, done, TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, elapsed
                                   );
            System.out.print("\r" + Strings.padRight(msg, 120));
            System.out.flush();
         }
         Pick chooseMRV() {
            Slot          best     = null;
            CandidateInfo bestInfo = null;
            int           bestSlot = -1;
            for (var s : slots) {
               if (assigned.containsKey(s.key())) continue;
               var entry = dictIndex[s.len()];
               if (entry == null) return PICK_NOT_DONE;
               patternForSlot(grid, s, ctx.pattern);
               var info = candidateInfoForPattern(ctx, entry, s.len());
               
               if (info.count == 0) return PICK_NOT_DONE;
               var slotScore = -1;
               if (best == null
                   || info.count < bestInfo.count
                   || (info.count == bestInfo.count && (slotScore = slotScore(count, s)) > bestSlot)) {
                  best     = s;
                  bestSlot = (slotScore != -1) ? slotScore : slotScore(count, s);
                  if (info.indices != null && (info.indices == ctx.inter1 || info.indices == ctx.inter2)) {
                     bestInfo = new CandidateInfo(Arrays.copyOf(info.indices, info.count), info.count);
                  } else {
                     bestInfo = info;
                  }
                  if (info.count <= 1) break;
               }
            }
            
            if (best == null) {
               return PICK_DONE;
            } else {
               return new Pick(best, bestInfo, false);
            }
         }
         boolean backtrack(int depth) {
            if (Thread.currentThread().isInterrupted()) return false;
            stats.nodes++;
            
            if (timeLimitMs > 0 && (System.currentTimeMillis() - t0) > timeLimitMs) return false;
            
            var pick = chooseMRV();
            if (pick.done) return true;
            if (pick.slot == null) {
               stats.backtracks++;
               return false;
            }
            val info = pick.info;
            stats.lastMRV = info.count;
            renderProgress();
            
            var s      = pick.slot;
            var k      = s.key();
            int patLen = s.len();
            var entry  = dictIndex[patLen];
            var pat    = new byte[patLen];
            patternForSlot(grid, s, pat);
            if (info.indices != null && info.indices.length > 0) {
               var idxs  = info.indices;
               var L     = idxs.length;
               var tries = Math.min(MAX_TRIES_PER_SLOT, L);
               
               for (var t = 0; t < tries; t++) {
                  double r          = rng.nextFloat();
                  int    idxInArray = (int) (r * r * r * L);
                  var    idx        = idxs[idxInArray];
                  var    w          = entry.words.get(idx);
                  
                  if (used.get(w.index())) continue;
                  
                  boolean match = true;
                  for (var i = 0; i < patLen; i++) {
                     if (pat[i] != DASH && pat[i] != w.byteAt(i)) {
                        match = false;
                        break;
                     }
                  }
                  
                  if (!match || !placeWord(grid, s, w, ctx.undo, depth)) continue;
                  
                  used.set(w.index());
                  assigned.put(k, w);
                  
                  if (backtrack(depth + 1)) return true;
                  
                  assigned.remove(k);
                  used.clear(w.index);
                  s.undoPlace(grid, ctx.undo[depth]);
               }
               stats.backtracks++;
               return false;
            }
            
            var N = entry.words.size();
            if (N == 0) {
               stats.backtracks++;
               return false;
            }
            
            var tries = Math.min(MAX_TRIES_PER_SLOT, N);
            for (var t = 0; t < tries; t++) {
               double r          = rng.nextFloat();
               int    idxInArray = (int) (r * r * r * N);
               var    w          = entry.words.get(idxInArray);
               
               if (used.get(w.index())) continue;
               
               boolean match = true;
               for (var i = 0; i < patLen; i++) {
                  if (pat[i] != DASH && pat[i] != w.byteAt(i)) {
                     match = false;
                     break;
                  }
               }
               if (!match || !placeWord(grid, s, w, ctx.undo, depth)) continue;
               
               used.set(w.index());
               assigned.put(k, w);
               
               if (backtrack(depth + 1)) return true;
               
               assigned.remove(k);
               used.clear(w.index);
               s.undoPlace(grid, ctx.undo[depth]);
            }
            
            stats.backtracks++;
            return false;
         }
      }
      
      // initial render (same feel)
      var solver = new Solver();
      solver.renderProgress();
      var ok = solver.backtrack(0);
      // final progress line
      if (!multiThreaded) {
         System.out.print("\r" + Strings.padRight("", 120) + "\r");
         System.out.flush();
      }
      
      stats.seconds = (System.currentTimeMillis() - t0) / 1000.0;
      var res = new FillResult(ok, new Gridded(grid), assigned, stats);
      
      // print a final progress line
      if (Main.VERBOSE && !multiThreaded) {
         System.out.println(
                 String.format(Locale.ROOT,
                               "[######################] %d/%d slots | nodes=%d | backtracks=%d | mrv=%d | %.1fs",
                               assigned.size(), TOTAL, stats.nodes, stats.backtracks, stats.lastMRV, stats.seconds
                              )
                           );
      }
      
      return res;
   }
}
