package puzzle;

import module java.base;
import anno.GenerateNeighbor;
import anno.GenerateNeighbors;
import anno.LemmaGen;
import lombok.val;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import precomp.Neighbors9x8;
import gen.rci;
import puzzle.DictJavaGeneratorMulti.DictEntryDTO.IntListDTO;
import puzzle.Export.Signa;
import puzzle.Export.Puzzle;
import puzzle.Export.Lettrix;
import puzzle.Masker.Slot;

import static org.junit.jupiter.api.Assertions.*;
import static precomp.Const9x8.*;
import static precomp.Const9x8.Cell.*;
import static puzzle.LemmaData.ABC;
import static puzzle.LemmaData.ABD;
import static puzzle.LemmaData.APPLE;
import static puzzle.LemmaData.APPLY;
import static puzzle.LemmaData.AT;
import static puzzle.LemmaData.AZ;
import static puzzle.LemmaData.BANAN;
import static puzzle.LemmaData.BANANA;
import static puzzle.LemmaData.BANANAS;
import static puzzle.LemmaData.BANANASS;
import static puzzle.LemmaData.CAT;
import static puzzle.LemmaData.DOGS;
import static puzzle.LemmaData.EXE;
import static puzzle.LemmaData.IN;
import static puzzle.LemmaData.INE;
import static puzzle.LemmaData.INER;
import static puzzle.LemmaData.INEREN;
import static puzzle.LemmaData.INERENA;
import static puzzle.LemmaData.INERENAE;
import static puzzle.LemmaData.WORD_A;
import static puzzle.LemmaData.WORD_C;
import static puzzle.LemmaData.WORD_X;
import static puzzle.SwedishGenerator.*;
@GenerateNeighbors(@GenerateNeighbor(C = 4, R = 3, packageName = "precomp", className = "Neighbors4x3", MIN_LEN = 2))
@LemmaGen(
        packageName = "puzzle",
        className = "LemmaData",
        words = {
                "EEN", "NAAR", "IEDEREEN", "A", "C", "X", "TEST", "IN", "INE", "INER", "INEREN", "INERENA", "INERENAE",
                "APPLE", "AXE", "ABC", "ABD", "AZ", "AB",
                "AT", "CAT", "DOGS", "APPLY", "BANAN", "BANANA", "BANANAS", "BANANASS"
        },
        minLen = 2,
        maxLen = 8
)
public class SwedishGeneratorTest {
   
   static Grid createEmpty9x8() { return new Grid(new byte[Neighbors9x8.SIZE], X, X); }
   record Context(long[] bitset) {
      
      public Context() { this(new long[2500]); }
      private static final ThreadLocal<Context> CTX = ThreadLocal.withInitial(Context::new);
      
      public static Context get() { return CTX.get(); }
   }
   
   static final long[] WORDS = new long[]{
           AT,
           CAT,
           DOGS,
           APPLE,
           APPLY,
           BANAN,
           BANANA,
           BANANAS,
           BANANASS
   };
   
   static final long[] WORDS2 = new long[]{ IN,
                                            APPLE,
                                            APPLY,
                                            BANAN,
                                            INE,
                                            INER,
                                            INEREN,
                                            INERENA,
                                            INERENAE };
   
   @Test
   void testPatternForSlotAllLetters() {
      var grid = new Puzzle(Signa.of(r0c0d1));
      GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).or(r0c3).lo(), X, ABC);
      val map = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(LETTER_A, map.get(OFF_0_1));
      assertEquals(LETTER_B, map.get(OFF_0_2));
      assertEquals(LETTER_C, map.get(OFF_0_3));
   }
   
   @Test
   void testPatternForSlotMixed() {
      var grid = createEmpty9x8();
      GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r0c0.mask, X, WORD_A);
      GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r2c0.mask, X, WORD_C);
      var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X);
      assertEquals(14081L, pattern);
   }
   
   @Test
   void testPatternForSlotAllDashes() {
      var grid    = createEmpty9x8();
      var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X);
      assertEquals(X, pattern);
   }
   
   @Test
   void testPatternForSlotSingleLetter() {
      var grid = createEmpty9x8();
      GridBuilder.placeWord(grid, grid.g, r0c0d1.slotKey, r0c0.mask, X, WORD_A);
      var pattern = patternForSlot(grid.lo, grid.hi, grid.g, 7L, X);
      assertEquals(1L, pattern);
   }
   
   @Test
   void testGrid() {
      var grid = new Puzzle(Clues.createEmpty());
      GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0d0.mask, X, WORD_A);
      val arr = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(1, arr.size());
      assertEquals(LETTER_A, arr.get(OFF_0_0));
   }
   
   @Test
   void testIntList() {
      var list = new IntListDTO();
      assertEquals(0, list.size());
      for (var i = 0; i < 10; i++) {
         list.add(i);
      }
      assertEquals(10, list.size());
      assertEquals(0, list.data()[0]);
      assertEquals(9, list.data()[9]);
   }
   
   @Test
   void testLemmaAndDict() {
      Assertions.assertEquals(Lemma.packShiftIn("APPLE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(APPLE));
      assertEquals(4, Lemma.unpackSize(APPLE));
      assertEquals(LETTER_I, Lemma.byteAt(INERENAE, 0));
      assertEquals(LETTER_N, Lemma.byteAt(INERENAE, 1));
      assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 2));
      assertEquals(LETTER_R, Lemma.byteAt(INERENAE, 3));
      assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 4));
      assertEquals(LETTER_N, Lemma.byteAt(INERENAE, 5));
      assertEquals(LETTER_A, Lemma.byteAt(INERENAE, 6));
      assertEquals(LETTER_E, Lemma.byteAt(INERENAE, 7));
      
      var dict = DictJavaGeneratorMulti.Dicts.makeDict(new long[]{ APPLE, EXE, IN, INER, INEREN, INERENA, INERENAE });
      
      assertEquals(1, dict.index()[3].words().length);
      assertEquals(1, dict.index()[5].words().length);
      
      var entry3 = dict.index()[3];
      assertEquals(1, entry3.words().length);
      assertEquals(Lemma.packShiftIn("AXE".getBytes(StandardCharsets.US_ASCII)), Lemma.unpackLetters(entry3.words()[0]));
   }
   
   @Test
   void testSlot() {
      assertEquals(OFF_2_3, Slot.clueIndex(r2c3d0.slotKey));
      assertEquals(CLUE_DOWN0, Slot.dir(r2c3d0.slotKey));
      assertFalse(Slot.horiz(r2c3d0.slotKey));
      var cells = Puzzle.cellWalk((byte) r2c3d0.slotKey, r2c5.or(r3c5).or(r4c5).lo(), 0L).mapToObj(i -> Masker.IT[i]).toArray(rci[]::new);
      assertEquals(2, cells[0].r());
      assertEquals(3, cells[1].r());
      assertEquals(4, cells[2].r());
      assertEquals(5, cells[0].c());
      assertEquals(5, cells[1].c());
      assertEquals(5, cells[2].c());
      
      assertTrue(Slot.horiz(CLUE_RIGHT1)); // right
      assertFalse(Slot.horiz(CLUE_DOWN0)); // down
   }
   
   static long packPattern(String s) {
      long p = 0;
      var  b = s.getBytes(StandardCharsets.US_ASCII);
      for (var i = 0; i < b.length; i++) {
         var val = b[i] & 31;
         if (val != 0) {
            p |= (i * 26L + val) << (i << 3);
         }
      }
      return p;
   }
   
   @Test
   void testCandidateInfoForPattern() {
      var dict = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2);
      
      // Pattern "APP--" for length 5
      var info = candidateInfoForPattern(Context.get().bitset(), packPattern("APP"), dict.index()[5].posBitsets(), dict.index()[5].numlong());
      
      assertEquals(2, info.length);
      assertNotNull(info);
   }
   
   @Test
   void testForEachSlotAndExtractSlots() {
      // This should detect a slot starting at 0,1 with length 2 (0,1 and 0,2)
      var dict  = DictJavaGeneratorMulti.Dicts.makeDict(WORDS2);
      var slots = Masker.extractSlots(Signa.of(r0c0d1).c(), dict.index(), dict.reversed());
      assertEquals(1, slots.length);
      var s = slots[0];
      
      assertTrue(Slot.length(s.lo(), s.hi()) >= 2);
      assertEquals(OFF_0_0, Slot.clueIndex(s.key()));
      assertEquals(CLUE_RIGHT1, Slot.dir(s.key()));
   }
   
   @Test
   void testMaskFitnessBasic() {
      var gen  = new Masker(new Rng(0), new int[Masker.STACK_SIZE], Clues.createEmpty());
      var grid = Clues.createEmpty();
      // Empty grid should have high penalty (no slots)
      var f1 = gen.maskFitness(grid, 18);
      assertTrue(f1 >= 1_000_000_000L);
      
      // Add a slot
      grid.setClue(r0c0d1);
      var f2 = gen.maskFitness(grid, 18);
      assertTrue(f2 < f1);
   }
   
   @Test
   void testGeneticAlgorithmComponents() {
      var gen = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Clues.createEmpty());
      var c1  = new Signa(gen.randomMask(18));
      assertNotNull(c1);
      var g2 = new Signa(gen.mutate(c1.deepCopyGrid().c()));
      assertNotNull(g2);
      assertNotSame(c1.c(), g2.c());
      assertNotNull(gen.crossover(c1.c(), g2.c()));
      assertNotNull(gen.hillclimb(c1.c(), 18, 10));
   }
   
   @Test
   void testPlaceWord() {
      var grid = new Puzzle(Clues.createEmpty());
      assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC));
      
      var map = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(3, map.size());
      assertEquals(LETTER_A, map.get(OFF_0_0));
      assertEquals(LETTER_B, map.get(OFF_0_1));
      assertEquals(LETTER_C, map.get(OFF_0_2));
      
      // 2. Successful placement with partial overlap (same characters)
      assertTrue(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC));
      // 3. Conflict: place "ABD" where "ABC" is
      assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABD));
      // Verify grid is unchanged (still "ABC")
      map = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(3, map.size());
      assertEquals(LETTER_A, map.get(OFF_0_0));
      assertEquals(LETTER_B, map.get(OFF_0_1));
      assertEquals(LETTER_C, map.get(OFF_0_2));
      
      // 4. Partial placement then conflict (rollback)
      grid = new Puzzle(Clues.createEmpty());
      GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, 1L << OFF_0_2, 0, WORD_X); // Conflict at the end
      assertFalse(GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c0.or(r0c1).or(r0c2).lo(), X, ABC));
      map = grid.stream().collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(1, map.size());
      assertEquals(LETTER_X, map.get(OFF_0_2));
   }
   
   @Test
   void testBacktrackingHelpers() {
      var grid = new Puzzle(Clues.createEmpty());
      // Slot at 0,1 length 2
      val low    = grid.grid().lo;
      val top    = grid.grid().hi;
      var placed = GridBuilder.placeWord(grid.grid(), grid.grid().g, r0c0d1.slotKey, r0c1.or(r0c2).lo(), X, AZ);
      assertTrue(placed);
      
      var map = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(2, map.size());
      assertEquals(LETTER_A, map.get(OFF_0_1));
      assertEquals(LETTER_Z, map.get(OFF_0_2));
      
      grid.grid().hi = top;
      grid.grid().lo = low;
      map            = grid.collect(Collectors.toMap(Lettrix::index, Lettrix::letter));
      assertEquals(0, map.size());
      assertFalse(map.containsKey(OFF_0_1));
      assertFalse(map.containsKey(OFF_0_2));
   }
   
   @Test
   void testInnerWorkings() {
      // 1. Test Slot.increasing
      assertFalse(Slotinfo.increasing(CLUE_LEFT3)); // Left
      assertTrue(Slotinfo.increasing(CLUE_RIGHT1));  // Right
      assertTrue(Slotinfo.increasing(CLUE_DOWN0));  // Down
      assertFalse(Slotinfo.increasing(CLUE_UP2)); // Up
      
      assertTrue(Slotinfo.increasing(r0c0d1.slotKey));
      assertFalse(Slotinfo.increasing(r0c0d3.slotKey));
      
      // 2. Test slotScore
      val counts = new byte[Neighbors9x8.SIZE];
      counts[1] = 2;
      counts[2] = 3;
      var dict   = DictJavaGeneratorMulti.Dicts.makeDict(WORDS);
      var entry5 = dict.index()[5];
      // cross = (counts[1]-1) + (counts[2]-1) = 1 + 2 = 3
      // score = 3 * 10 + len(2) = 32
      assertEquals(32, Masker.slotScore(counts, (1L << 1) | (1L << 2), 0L));
      // 3. Test candidateCountForPattern
      
      var ctx     = Context.get();
      var pattern = packPattern("APP");
      assertEquals(2, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong()));
      
      pattern = packPattern("BAN");
      assertEquals(1, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong()));
      
      pattern = packPattern("CAT");
      assertEquals(0, candidateCountForPattern(ctx.bitset(), pattern, entry5.posBitsets(), entry5.numlong()));
   }
   
   @Test
   void testMaskFitnessDetailed() {
      var gen  = new Masker(new Rng(42), new int[Masker.STACK_SIZE], Clues.createEmpty());
      var grid = Clues.createEmpty();
      // Empty grid: huge penalty
      var fitEmpty = gen.maskFitness(grid, 18);
      assertTrue(fitEmpty >= 1_000_000_000L);
      grid.setClue(r0c0d1); // Right from 0,0. Len 2 if 3x3.
      var fitOne = gen.maskFitness(grid, 18);
      assertTrue(fitOne < fitEmpty);
   }
   @Test
   void testRng() {
      var rng  = new Rng(123);
      var val1 = rng.nextU32();
      var val2 = rng.nextU32();
      assertNotEquals(val1, val2);
      
      var rng2 = new Rng(123);
      assertEquals(val1, rng2.nextU32());
      
      for (var i = 0; i < 100; i++) {
         var r = rng.randomClueDir();
         assertTrue(r >= 0 && r <= 5);
         var f = rng.nextFloat();
         assertTrue(f >= 0.0 && f <= 1.0);
      }
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
      assertTrue(rng.biasedIndexPow3(100) >= 0 && rng.biasedIndexPow3(100) < 100);
   }
   
}