package puzzle;

import anno.ConstGen;
import anno.GenerateNeighbor;
import anno.GenerateNeighbors;
import anno.Shaped;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.val;
import precomp.Neighbors9x8;
import static java.lang.Long.bitCount;
import static java.lang.Long.numberOfLeadingZeros;
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]
 */
@ConstGen(C = 9, R = 8, packageName = "precomp", className = "Const9x8")
@GenerateNeighbors({
        @GenerateNeighbor(C = 9, R = 8, packageName = "precomp", className = "Neighbors9x8", MIN_LEN = 2),
        @GenerateNeighbor(C = 4, R = 3, packageName = "precomp", className = "Neighbors4x3", MIN_LEN = 2)
})

public record SwedishGenerator() {
   
   public static final          int  MAX_TRIES_PER_SLOT = 500;// MAX_TRIES_PER_SLOT;
   public static final          long X                  = 0L;
   @Shaped private static final int  SIZE               = Neighbors9x8.SIZE;
   @Shaped private static final long RANGE_0_SIZE       = Neighbors9x8.RANGE_0_SIZE;
   @Shaped private static final long RANGE_0_624        = Neighbors9x8.RANGE_0_624;
   
   interface Bit1029 {
      
      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); }
   }
   
   //@formatter:off
                                       public record      Dict(DictEntry[] index,DictEntry[] reversed, int length) {      public Dict(DictEntry[] index,int length){this(index,index,length);} }
                                       public record      DictEntry(long[] words, long[][] posBitsets, int length, int numlong) { }
   @AllArgsConstructor   @NoArgsConstructor public  static final class Assign    {      long w;   }
   @AllArgsConstructor                 public static final class Grid      {      public final byte[] g; public       long   lo, hi;    }
                                       public record      FillResult(boolean ok, long nodes, long backtracks, int lastMRV, long elapsed ) { }
   //@formatter:on
   
   public static final class Rng {
      
      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;
      }
      static final byte[] BYTE = new byte[]{ 0, 1, 2, 3, 4, 5 };
      public byte randomClueDir()       { return rand(BYTE); }
      public <T> T rand(T[] p)          { return p[(int) (((nextU32() & 0xFFFFFFFFL) % ((long) p.length)))]; }
      public byte rand(byte[] p)        { return p[(int) (((nextU32() & 0xFFFFFFFFL) % ((long) p.length)))]; }
      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); }
   }
   
   public interface Lemma {
      
      long LETTER_MASK            = (1L << 40) - 1;     // low 40 bits
      long LETTER_AND_LENGTH_MASK = (1L << 43) - 1;
      
      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 packShiftIn(byte[] b) {
         long w = 0;
         for (var i = b.length - 1; i >= 0; i--) w = (w << 5) | ((long) b[i] & 31);
         return w;
      }
      static 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 >>> (idx * 5)) & 0b11111L); }
      static int length0(long word)          { return ((63 - numberOfLeadingZeros(word & LETTER_MASK)) / 5); }
      static String asWord(long word, byte[] bytes) {
         var bi = 0;
         for (var w = word & LETTER_MASK; w != 0; w >>>= 5) bytes[bi++] = (byte) ((w & 31) | 64);
         return new String(bytes, 0, bi, US_ASCII);
      }
      static int unpackIndex(long w)              { return (int) (w >>> 40); }
      static int unpackSize(long w)               { return (int) (w >>> 40) & 7; }
      static int unpackLetters(long w)            { return (int) (w & LETTER_MASK); }
      static long packLetterAndLengthBits(long w) { return w & LETTER_AND_LENGTH_MASK; }
   }
   
   public record Slotinfo(int key, long lo, long hi, int score, Assign assign, DictEntry entry, int minL) {
      
      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);
      }
   }
   
   public static long patternForSlot(final long glo, final long ghi, final byte[] g, final long lo, final long hi) {
      if (((lo & glo) == X) && (hi & ghi) == X) return X;
      long p = 0;
      int  n = 0, offset, idx;
      for (var b = lo & glo; b != X; b &= b - 1) {
         idx = numberOfTrailingZeros(b);
         p |= ((long) (bitCount(lo & ((1L << idx) - 1)) * 26 + g[idx])) << (n++ << 3);
      }
      offset = bitCount(lo);
      for (var b = hi & ghi; b != X; b &= b - 1) {
         idx = numberOfTrailingZeros(b);
         p |= ((long) ((offset + bitCount(hi & ((1L << idx) - 1))) * 26 + g[64 | idx])) << (n++ << 3);
      }
      return p;
   }
   
   /// pattern cannot be X
   public static int[] candidateInfoForPattern(long[] res, long pattern, long[][] posBitsets, int numLongs) {
      var indices = new int[candidateCountForPattern(res, pattern, posBitsets, numLongs)];
      for (int k = 0, ki = 0; k < numLongs; k++) for (var 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[][] pos, final int num) {
      System.arraycopy(pos[(int) (pattern & 0xFF) - 1], 0, res, 0, num);
      for (var p = pattern >>> 8; p != X; p >>>= 8) {
         var bs = pos[(int) (p & 0xFF) - 1];
         for (var k = 0; k < num; k++) res[k] &= bs[k];
      }
      
      var count = 0;
      for (var k = 0; k < num; k++) count += bitCount(res[k]);
      return count;
   }
   
   public static FillResult fillMask(final Rng rng, final Slotinfo[] slots,
                                     final long lo, final long hi, final byte[] g) {
      val used   = new long[2048];
      val bitset = new long[2500];
      val TOTAL  = slots.length;
      val t0     = System.currentTimeMillis();
      class Solver {
         
         static final int PICK_NOT_DONE = -1;
         static final int PICK_DONE     = 0;
         long nodes;
         long tracks;
         long glo = lo, ghi = hi;
         Slotinfo slot;
         int[]    indices;
         boolean place(final long lo, final long hi, final long w) {
            int idx;
            for (var b = lo & glo; b != X; b &= b - 1) if (g[idx = numberOfTrailingZeros(b)] != Lemma.byteAt(w, bitCount(lo & ((1L << idx) - 1)))) return true;
            var bcLo = bitCount(lo);
            for (var b = hi & ghi; b != X; b &= b - 1) if (g[64 | (idx = numberOfTrailingZeros(b))] != Lemma.byteAt(w, bcLo + bitCount(hi & ((1L << idx) - 1)))) return true;
            
            long maskLo = lo & ~glo, maskHi = hi & ~ghi;
            if ((maskLo | maskHi) != X) {
               for (var b = maskLo; b != X; b &= b - 1) g[idx = numberOfTrailingZeros(b)] = Lemma.byteAt(w, bitCount(lo & ((1L << idx) - 1)));
               for (var 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 false;
         }
         int chooseMRV() {
            Slotinfo best = null;
            int      max  = -1, bestScore = -1;
            for (int i = 0, n = TOTAL; i < n; i++) {
               var s = slots[i];
               if (s.assign.w != X) continue;
               var pattern = patternForSlot(glo, ghi, g, s.lo, s.hi);
               var index   = s.entry;
               var count   = pattern == X ? index.length : candidateCountForPattern(bitset, pattern, index.posBitsets, index.numlong);
               
               if (count == 0) return PICK_NOT_DONE;
               if (best == null
                   || count < max
                   || (count == max && s.score > bestScore)) {
                  best      = s;
                  bestScore = s.score;
                  max       = count;
                  if (count <= 1) break;
               }
            }
            if (best == null) return PICK_DONE;
            var pattern = patternForSlot(glo, ghi, g, best.lo, best.hi);
            slot = best;
            var index = best.entry;
            indices = pattern == X ? null : candidateInfoForPattern(bitset, pattern, index.posBitsets, index.numlong);
            return 1;
         }
         boolean backtrack(int depth) {
            if (Thread.currentThread().isInterrupted() || (System.currentTimeMillis() - t0) > 20_000) return false;
            nodes++;
            
            var status = chooseMRV();
            if (status == PICK_DONE) return true;
            if (status == PICK_NOT_DONE) {
               tracks++;
               return false;
            }
            val  info   = indices;
            val  s      = slot;
            val  slo    = s.lo;
            val  shi    = s.hi;
            val  assign = s.assign;
            val  words  = s.entry.words;
            long low, top;
            if (info != null && info.length > 0) {
               var L     = info.length;
               var tries = Math.min(MAX_TRIES_PER_SLOT, L);
               
               for (var t = 0; t < tries; t++) {
                  var w      = words[info[rng.biasedIndexPow3(L - 1)]];
                  var lemIdx = Lemma.unpackIndex(w);
                  if (Bit1029.get(used, lemIdx)) continue;
                  low = glo;
                  top = ghi;
                  if (place(slo, shi, w)) continue;
                  
                  Bit1029.set(used, lemIdx);
                  assign.w = w;
                  if (backtrack(depth + 1)) return true;
                  assign.w = X;
                  Bit1029.clear(used, lemIdx);
                  glo = low;
                  ghi = top;
               }
               tracks++;
               return false;
            }
            
            var N = words.length;
            
            for (var t = 0; t < s.minL; t++) {
               var w   = words[rng.biasedIndexPow3(N - 1)];
               var lem = Lemma.unpackIndex(w);
               if (Bit1029.get(used, lem)) continue;
               low = glo;
               top = ghi;
               if (place(slo, shi, w)) continue;
               
               Bit1029.set(used, lem);
               assign.w = w;
               if (backtrack(depth + 1)) return true;
               assign.w = X;
               Bit1029.clear(used, lem);
               
               glo = low;
               ghi = top;
            }
            
            tracks++;
            return false;
         }
      }
      var solver = new Solver();
      var ok     = solver.backtrack(0);
      return new FillResult(ok, solver.nodes, solver.tracks, solver.slot == null ? 0 : solver.slot.entry.length, System.currentTimeMillis() - t0);
   }
}
