package puzzle;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.Delegate;
import lombok.val;
import precomp.Neighbors9x8;
import precomp.Neighbors9x8.rci;
import java.util.List;
import java.util.Locale;
import static java.lang.Long.*;
import static java.lang.Long.numberOfTrailingZeros;
import static java.nio.charset.StandardCharsets.US_ASCII;

/**
 * NOTE:
 * A) generate randoms for and idx and direction (clue together)
 * B) convert IDX_PATH_0_BASE into bit-pattern (i think) Length should not be checked, just left empty if not good.
 * C) store more information in the byte[] even consider the long[] or try sqeeze the 288 options (clue) in a byte.
 * F) pre-determine random clue arrangements, so they do not involve impossible clue's such as bottom at last or the line before that and such to all sides
 * G) Check 3wall, the current implementation may be faster, but the accuracy is lower, degrade performance on the filler
 */

/**
 * 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() {
   
   public static final long   X                        = 0L;
   public static final int    LOG_EVERY_MS             = 200;
   public static final int    BAR_LEN                  = 22;
   public static final int    C                        = Config.PUZZLE_COLS;
   public static final double CROSS_R                  = (C - 1) / 2.0;
   public static final int    R                        = Config.PUZZLE_ROWS;
   public static final double CROSS_C                  = (R - 1) / 2.0;
   public static final int    SIZE                     = C * R;// ~18
   public static final int    SIZE_MIN_1               = SIZE - 1;// ~18
   public static final double SIZED                    = (double) SIZE;// ~18
   public static final long   MASK_LO                  = (SIZE >= 64) ? -1L : (1L << SIZE) - 1;
   public static final long   MASK_HI                  = (SIZE <= 64) ? 0L : (SIZE >= 128 ? -1L : (1L << (SIZE - 64)) - 1);
   public static final int    MAX_WORD_LENGTH          = C <= R ? C : R;
   public static final int    MAX_WORD_LENGTH_PLUS_ONE = MAX_WORD_LENGTH + 1;
   public static final int    MIN_LEN                  = 3;//Config.MIN_LEN;
   public static final int    MAX_TRIES_PER_SLOT       = 700;//Config.MAX_TRIES_PER_SLOT;
   public static final int    STACK_SIZE               = 128;
   public static final char   C_DASH                   = '\0';
   public static final byte   DASH                     = (byte) C_DASH;
   public static final long   RANGE_0_SIZE             = (long) SIZE_MIN_1 - 0L + 1L;
   public static final long   RANGE_0_624              = 624L - 0L + 1L;
   public static int clamp(int x, int a, int b) { return Math.max(a, Math.min(b, x)); }
   interface Bit1029 {
      
      static long[] bit1029()                       { return new long[2048]; }
      private static int wordIndex(int bitIndex)    { return bitIndex >> 6; }
      static boolean get(long[] bits, int bitIndex) { return (bits[wordIndex(bitIndex)] & 1L << bitIndex) != X; }
      static void set(long[] bits, int bitIndex)    { bits[wordIndex(bitIndex)] |= 1L << bitIndex; }
      static void clear(long[] bits, int bitIndex)  { bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); }
   }
   static String padRight(String s, int n) {
      if (s.length() >= n) return s;
      return s + " ".repeat(n - s.length());
   }
   @AllArgsConstructor
   public static class Pick {
      
      public Slotinfo slot;
      public int[]    indices;
      public int      count;
   }
   
   public static final long[] OFFSETS_D_IDX;
   public static final rci[]  IT             = Neighbors9x8.IT;
   public static final long[] NBR8_PACKED_LO = Neighbors9x8.NBR8_PACKED_LO;
   public static final long[] NBR8_PACKED_HI = Neighbors9x8.NBR8_PACKED_HI;
   public static final long[] PATH_LO;
   public static final long[] PATH_HI;
   
   static {
      final int NEW_SIZE = SIZE << Masker.Slot.BIT_FOR_DIR;
      PATH_LO = new long[NEW_SIZE];
      PATH_HI = new long[NEW_SIZE];
      OFFSETS_D_IDX = new long[NEW_SIZE];
      for (int i = 0; i < SIZE; i++) {
         for (int d = 0; d < 4; d++) {
            int oldKey = (i << 2) | d;
            int newKey = (i << Masker.Slot.BIT_FOR_DIR) | d;
            PATH_LO[newKey] = Neighbors9x8.PATH_LO[oldKey];
            PATH_HI[newKey] = Neighbors9x8.PATH_HI[oldKey];
            OFFSETS_D_IDX[newKey] = Neighbors9x8.OFFSET_D_IDX_0_BASE[oldKey];
         }
         // Richting 4: Corner Down (Clue op r,c -> Woord op r,c+1 naar beneden)
         int r = IT[i].r();
         int c = IT[i].c();
         int newKey4 = (i << Masker.Slot.BIT_FOR_DIR) | 4;
         if (c + 1 < C) {
            long mLo = 0, mHi = 0;
            for (int ri = r; ri < R; ri++) {
               int target = Grid.offset(ri, c + 1);
               if (isLo(target)) mLo |= (1L << target);
               else mHi |= (1L << (target & 63));
            }
            PATH_LO[newKey4] = mLo;
            PATH_HI[newKey4] = mHi;
            int first = Grid.offset(r, c + 1);
            int last  = Grid.offset(R - 1, c + 1);
            OFFSETS_D_IDX[newKey4] = (long)first | ((long)last << 7);
         }
      }
   }
   public static final Pick   PICK_DONE      = null;//new Pick(null, null, 0, true);
   public static final Pick   PICK_NOT_DONE  = new Pick(null, null, 0);
   
   @RequiredArgsConstructor
   public static final class FillStats {
      
      public double simplicity;
   }
   
   public static record FillResult(boolean ok, long nodes, long backtracks, int lastMRV, long elapsed, FillStats stats) {
      
   }
   
   public static final class Rng {
      
      @Getter private int x;
      public Rng(int seed) {
         var s = seed;
         if (s == 0) s = 1;
         this.x = s;
      }
      public int nextU32() {
         var y = x;
         y ^= (y << 13);
         y ^= (y >>> 17);
         y ^= (y << 5);
         x = y;
         return y;
      }
      public byte randint2bitByte() {
         int r = nextU32() & 7;
         if (r < 4) return (byte) r;
         if (r == 4) return (byte) 4;
         return (byte) (r % 4);
      }
      public <T> T rand(T[] p)          { return p[(int) (((nextU32() & 0xFFFFFFFFL) % ((long) p.length /*- 0L*/ /*+ 1L*/)))]; }
      public <T> T rand(List<T> p)      { return p.get((int) (((nextU32() & 0xFFFFFFFFL) % ((long) p.size() /*- 0L*/ /*+ 1L*/)))); }
      public int randint(int max)       { return (int) (((nextU32() & 0xFFFFFFFFL) % ((long) max /*- 0L*/ /*+ 1L*/))); }
      public int randint0_SIZE()        { return (int) (((nextU32() & 0xFFFFFFFFL) % RANGE_0_SIZE)); }
      public int randint0_624()         { return (int) (((nextU32() & 0xFFFFFFFFL) % RANGE_0_624)); }
      public double nextFloat()         { return (nextU32() & 0xFFFFFFFFL) / 4294967295.0; }
      public int biasedIndexPow3(int N) { return (int) (((Math.min(nextU32(), Math.min(nextU32(), nextU32())) & 0xFFFFFFFFL) * (long) N) >>> 32); }
   }
   
   @AllArgsConstructor
   public static class Grid {
      
      public final byte[] g;
      public       long   lo, hi;
      public static int offset(int r, int c) { return r | (c << 3); }
      
      public Grid copy() {
         return new Grid(g.clone(), lo, hi);
      }
   }
   
   public static record DictEntry(long[] words, long[][] posBitsets, int length, int numlong) { }
   
   public static interface Lemma {
      
      static final long LETTER_MASK = (1L << 40) - 1;     // low 40 bits
      static final long INDEX_MASK  = (1L << 24) - 1;     // 24 bits
      
      static long from(byte[] word)            { return packShiftIn(word) | ((long) (word.length - 1) << 40); }
      static long pack(long w, int shardIndex) { return w | (((long) shardIndex) << 43) | ((long) length0(w)) << 40; }
      /*    static long pack(byte[] b) {
             long w = 0;
             for (var i = 0; i < b.length; i++) w |= ((long) b[i] & 31) << (i * 5);
             return w;
          }*/
      static long packShiftIn(byte[] b) {
         long w = 0;
         for (int i = b.length - 1; i >= 0; i--) w = (w << 5) | ((long) b[i] & 31);
         return w;
      }
      static public long from(String word)   { return packShiftIn(word.getBytes(US_ASCII)) | ((long) (word.length() - 1) << 40); }
      static byte byteAt(long word, int idx) { return (byte) ((word >>> ((long) idx * 5L)) & 0b11111L); }
      static int length0(long word)          { return ((63 - numberOfLeadingZeros(word & LETTER_MASK)) / 5); }
      static ThreadLocal<byte[]> BYTES = ThreadLocal.withInitial(() -> new byte[MAX_WORD_LENGTH]);
      public static String asWord(long word) {
         var b  = BYTES.get();
         int bi = 0;
         
         for (long w = word & LETTER_MASK; w != 0; w >>>= 5) {
            b[bi++] = (byte) ((w & 31) | 64); // neem laagste 5 bits
         }
         return new String(b, 0, bi, US_ASCII);
      }
      static int unpackIndex(long w)      { return (int) (w >>> 40); }
      static int unpackShardIndex(long w) { return (int) (w >>> 43); }
      static int unpackSize(long w)       { return (int) (w >>> 40) & 7; }
      static int unpackLetters(long w)    { return (int) (w & LETTER_MASK); }
   }
   
   public static record Dict(DictEntry[] index, int length) { }
   
   @AllArgsConstructor
   @NoArgsConstructor
   static class Assign {
      
      long w;
   }
   
   public static record Slotinfo(int key, long lo, long hi, int score, Assign assign, DictEntry entry) {
      
      public static int wordCount(int k, Slotinfo[] arr) {
         for (var n = 1; n < arr.length; n++) if (arr[n].assign.w != X) k++;
         return k;
      }
      public static boolean increasing(int dir) { return (dir & 2) == 0; }
      public static Grid grid(Slotinfo[] slots) {
         long lo = X, hi = X;
         for (var slot : slots) {
            lo |= slot.lo;
            hi |= slot.hi;
         }
         return new Grid(new byte[SIZE], ~lo, ~hi /*& 0xFF*/);
      }
   }
   
   public static boolean isLo(int n) { return (n & 64) == 0; }
   public static long patternForSlot(final long glo, final long ghi, final byte[] g, final int key, final long lo, final long hi) {
      if (((lo & glo) | (hi & ghi)) == X) return 0;
      long p = 0;
      int  n = 0;
      if (Slotinfo.increasing(key)) {
         for (long b = lo & glo; b != X; b &= b - 1) {
            int idx = numberOfTrailingZeros(b);
            int i   = bitCount(lo & ((1L << idx) - 1));
            p |= ((long) (i * 26 + g[idx])) << (n++ << 3);
         }
         int offset = bitCount(lo);
         for (long b = hi & ghi; b != X; b &= b - 1) {
            int idx = numberOfTrailingZeros(b);
            int i   = offset + bitCount(hi & ((1L << idx) - 1));
            p |= ((long) (i * 26 + g[64 | idx])) << (n++ << 3);
         }
      } else {
         int offset = bitCount(hi);
         for (long b = hi & ghi; b != X; b &= b - 1) {
            int idx = numberOfTrailingZeros(b);
            int i   = bitCount(hi & ~((1L << idx) | ((1L << idx) - 1)));
            p |= ((long) (i * 26 + g[64 | idx])) << (n++ << 3);
         }
         for (long b = lo & glo; b != X; b &= b - 1) {
            int idx = numberOfTrailingZeros(b);
            int i   = offset + bitCount(lo & ~((1L << idx) | ((1L << idx) - 1)));
            p |= ((long) (i * 26 + g[idx])) << (n++ << 3);
         }
      }
      return p;
   }
   
   /// pattern cannot be X
   public static int[] candidateInfoForPattern(long[] res, long pattern, long[][] posBitsets, int numLongs) {
      System.arraycopy(posBitsets[(int) (pattern & 0xFF) - 1], 0, res, 0, numLongs);
      for (long p = pattern >>> 8; p != X; p >>>= 8) {
         long[] bs = posBitsets[(int) (p & 0xFF) - 1];
         for (int k = 0; k < numLongs; k++) res[k] &= bs[k];
      }
      
      int count = 0;
      for (int k = 0; k < numLongs; k++) count += bitCount(res[k]);
      
      int[] indices = new int[count];
      for (int k = 0, ki = 0; k < numLongs; k++) {
         for (long w = res[k]; w != X; w &= w - 1) indices[ki++] = (k << 6) | numberOfTrailingZeros(w);
      }
      
      return indices;
   }
   /// pattern cannot be X
   public static int candidateCountForPattern(final long[] res, final long pattern, final long[][] posBitsets, final int numLongs) {
      System.arraycopy(posBitsets[(int) (pattern & 0xFF) - 1], 0, res, 0, numLongs);
      for (long p = pattern >>> 8; p != X; p >>>= 8) {
         long[] bs = posBitsets[(int) (p & 0xFF) - 1];
         for (int k = 0; k < numLongs; k++) res[k] &= bs[k];
      }
      
      int count = 0;
      for (int k = 0; k < numLongs; k++) count += bitCount(res[k]);
      return count;
   }
   
   public static FillResult fillMask(final Rng rng, final Slotinfo[] slots,
                                     final Grid grid,
                                     final boolean NO_LOG) {
      val used    = new long[2048];
      val bitset  = new long[2500];
      val g       = grid.g;
      val TOTAL   = slots.length;
      val t0      = System.currentTimeMillis();
      val CARRIER = new Pick(null, null, 0);
      class Solver {
         
         long nodes;
         long backtracks;
         int  lastMRV;
         long lastLog = t0, glo = grid.lo, ghi = grid.hi;
         Pick current = CARRIER;
         void renderProgress() {
            var now = System.currentTimeMillis();
            if ((now - lastLog) < LOG_EVERY_MS) return;
            lastLog = (now);
            var done = 0;
            for (var lemma : slots) {
               if (lemma.assign.w != X) done++;
            }
            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, nodes, backtracks, lastMRV, elapsed
                                   );
            System.out.print("\r" + padRight(msg, 120));
            System.out.flush();
         }
         boolean placeWordDec(final long lo, final long hi, final long w) {
            int idx;
            int bcHi = bitCount(hi);
            for (long b = hi & ghi; b != X; b &= b - 1)
               if (g[64 | (idx = numberOfTrailingZeros(b))] != Lemma.byteAt(w, bitCount(hi & ~((1L << idx) | ((1L << idx) - 1))))) return false;
            for (long b = lo & glo; b != X; b &= b - 1)
               if (g[idx = numberOfTrailingZeros(b)] != Lemma.byteAt(w, bcHi + bitCount(lo & ~((1L << idx) | ((1L << idx) - 1))))) return false;
            
            long maskLo = lo & ~glo, maskHi = hi & ~ghi;
            if ((maskLo | maskHi) != X) {
               for (long b = maskHi; b != X; b &= b - 1) g[64 | (idx = numberOfTrailingZeros(b))] = Lemma.byteAt(w, bitCount(hi & ~((1L << idx) | ((1L << idx) - 1))));
               for (long b = maskLo; b != X; b &= b - 1) g[idx = numberOfTrailingZeros(b)] = Lemma.byteAt(w, bcHi + bitCount(lo & ~((1L << idx) | ((1L << idx) - 1))));
               glo |= maskLo;
               ghi |= maskHi;
            }
            return true;
         }
         boolean placeWordInc(final long lo, final long hi, final long w) {
            int idx;
            for (long b = lo & glo; b != X; b &= b - 1) if (g[idx = numberOfTrailingZeros(b)] != Lemma.byteAt(w, bitCount(lo & ((1L << idx) - 1)))) return false;
            int bcLo = bitCount(lo);
            for (long b = hi & ghi; b != X; b &= b - 1) if (g[64 | (idx = numberOfTrailingZeros(b))] != Lemma.byteAt(w, bcLo + bitCount(hi & ((1L << idx) - 1)))) return false;
            
            long maskLo = lo & ~glo, maskHi = hi & ~ghi;
            if ((maskLo | maskHi) != X) {
               for (long b = maskLo; b != X; b &= b - 1) g[idx = idx = numberOfTrailingZeros(b)] = Lemma.byteAt(w, bitCount(lo & ((1L << idx) - 1)));
               for (long b = maskHi; b != X; b &= b - 1) g[64 | (idx = numberOfTrailingZeros(b))] = Lemma.byteAt(w, bcLo + bitCount(hi & ((1L << idx) - 1)));
               glo |= maskLo;
               ghi |= maskHi;
            }
            return true;
         }
         void chooseMRV() {
            Slotinfo best = null;
            for (int i = 0, count, count2 = -1, bestScore = -1, n = TOTAL; i < n; i++) {
               var s = slots[i];
               if (s.assign.w != X) continue;
               var pattern = patternForSlot(glo, ghi, g, s.key, s.lo, s.hi);
               var index   = s.entry;
               count = pattern == X ? index.length : candidateCountForPattern(bitset, pattern, index.posBitsets, index.numlong);
               
               if (count == 0) {
                  current = PICK_NOT_DONE;
                  return;
               }
               if (best == null
                   || count < count2
                   || (count == count2 && s.score > bestScore)) {
                  best      = s;
                  bestScore = s.score;
                  count2    = count;
                  if (count <= 1) break;
               }
            }
            // Re-calculate for the best slot to get actual indices
            if (best == null) {
               current = PICK_DONE;
               return;
            }
            var pattern = patternForSlot(glo, ghi, g, best.key, best.lo, best.hi);
            var index   = best.entry;
            current         = CARRIER;
            current.slot    = best;
            current.count   = index.length;
            current.indices = pattern == X ? null : candidateInfoForPattern(bitset, pattern, index.posBitsets, index.numlong);
         }
         boolean backtrack(int depth) {
            if (Thread.currentThread().isInterrupted() || (System.currentTimeMillis() - t0) > 20_000) return false;
            nodes++;
            
            chooseMRV();
            var pick = current;
            if (pick == PICK_DONE) return true;
            if (pick.slot == null) {
               backtracks++;
               return false;
            }
            val info = pick.indices;
            lastMRV = pick.count;
            if (!NO_LOG) renderProgress();
            
            val  s     = pick.slot;
            val  inc   = Slotinfo.increasing(s.key);
            val  slo   = s.lo;
            val  shi   = s.hi;
            val  entry = s.entry;
            long low, top;
            if (info != null && info.length > 0) {
               var idxs  = info;
               var L     = idxs.length;
               var tries = Math.min(MAX_TRIES_PER_SLOT, L);
               
               for (var t = 0; t < tries; t++) {
                  //var r          = rng.nextFloat();
                  //var idxInArray = (int) (r * r * r * (L - 1));
                  int idxInArray = rng.biasedIndexPow3(L - 1);
                  var w          = entry.words[idxs[idxInArray/*(int) (r * r * r * (L - 1))*/]];
                  var lemIdx     = Lemma.unpackIndex(w);
                  if (Bit1029.get(used, lemIdx)) continue;
                  low = glo;
                  top = ghi;
                  if (inc) {
                     if (!placeWordInc(slo, shi, w)) continue;
                  } else {
                     if (!placeWordDec(slo, shi, w)) continue;
                  }
                  
                  Bit1029.set(used, lemIdx);
                  s.assign.w = w;
                  if (backtrack(depth + 1)) return true;
                  s.assign.w = X;
                  Bit1029.clear(used, lemIdx);
                  glo = low;
                  ghi = top;
               }
               backtracks++;
               return false;
            }
            
            var N = entry.words.length;
            
            var tries = Math.min(MAX_TRIES_PER_SLOT, N);
            for (var t = 0; t < tries; t++) {
               // double r        = rng.nextFloat();
               var w      = entry.words[rng.biasedIndexPow3(N - 1)/*(int) (r * r * r * (N - 1))*/];
               var lemIdx = Lemma.unpackIndex(w);
               if (Bit1029.get(used, lemIdx)) continue;
               low = glo;
               top = ghi;
               if (inc) {
                  if (!placeWordInc(slo, shi, w)) continue;
               } else {
                  if (!placeWordDec(slo, shi, w)) continue;
               }
               
               Bit1029.set(used, lemIdx);
               s.assign.w = w;
               if (backtrack(depth + 1)) return true;
               s.assign.w = X;
               Bit1029.clear(used, lemIdx);
               
               glo = low;
               ghi = top;
            }
            
            backtracks++;
            return false;
         }
      }
      
      // initial render (same feel)
      var solver = new Solver();
      if (!NO_LOG) solver.renderProgress();
      var ok = solver.backtrack(0);
      // final progress line
      grid.lo = solver.glo;
      grid.hi = solver.ghi;
      
      return new FillResult(ok, solver.nodes, solver.backtracks, solver.lastMRV, System.currentTimeMillis() - t0, new FillStats());
   }
}
