package puzzle;

import module java.base;
import lombok.AllArgsConstructor;
import lombok.experimental.Delegate;
import lombok.val;
import precomp.Const9x8.Cell;
import puzzle.Export.Puzzle.Replacar.Rell;
import puzzle.Meta.ShardLem;
import puzzle.SwedishGenerator.Dict;
import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Grid;
import puzzle.SwedishGenerator.Slotinfo;
import static precomp.Const9x8.CLUE_DOWN0;
import static precomp.Const9x8.CLUE_RIGHT1;
import static precomp.Const9x8.INIT_GRID_OUTPUT;
import static precomp.Const9x8.INIT_GRID_OUTPUT_ARR;
import static puzzle.Export.Clue.DOWN0;
import static puzzle.Export.Clue.RIGHT1;
import static puzzle.Clues.createEmpty;
import static puzzle.Masker.Slot;
import static puzzle.Masker.C;
import static puzzle.SwedishGenerator.Lemma;
import static puzzle.SwedishGenerator.X;

/**
 * ExportFormat.java
 *
 * Direct port of export_format.js:
 * - scans filled grid for clue digits '1'..'4'
 * - extracts placed words in canonical direction (horizontal=right, vertical=down)
 * - crops to bounding box (words + arrow cells) with 1-cell margin
 * - outputs gridv2 + words[] (+ difficulty, rewards)
 */
public record Export() {
   
   public static final ThreadLocal<byte[]> BYTES          = ThreadLocal.withInitial(() -> new byte[8]);
   static final        byte                CLUE_UP        = 2;
   static final        byte                CLUE_LEFT      = 3;
   static final        byte                CLUE_LEFT_TOP  = 4;
   static final        byte                CLUE_RIGHT_TOP = 5;
   static int HI(int in)                    { return in | 64; }
   static char LETTER(int in)               { return (char) (in | 64); }
   static char CLUE_CHAR(int s)             { return (char) (s | 48); }
   static int INDEX_ROW(int idx)            { return idx & 7; }
   static int INDEX_COL(int idx)            { return idx >>> 3; }
   static int INDEX(int r, int cols, int c) { return r * cols + c; }
   @AllArgsConstructor
   enum Clue {
      DOWN0(CLUE_DOWN0, 'B', 'b'),
      RIGHT1(CLUE_RIGHT1, 'A', 'a'),
      UP2(CLUE_UP, 'C', 'c'),
      LEFT3(CLUE_LEFT, 'D', 'd'),
      LEFT_TOP4(CLUE_LEFT_TOP, 'E', 'e'),
      RIGHT_TOP5(CLUE_RIGHT_TOP, 'F', 'f'),
      NONE(CLUE_LEFT, '?', '?');
      final byte dir;
      final char slotChar, clueChar;
      private static final Clue[] CLUES = new Clue[]{ DOWN0, RIGHT1, UP2, LEFT3, LEFT_TOP4, RIGHT_TOP5, NONE, NONE, NONE };
      public static Clue from(int dir) { return CLUES[dir]; }
   }
   
   public record Vestigium(int index, int clue) { }
   
   public record Signa(@Delegate Clues c) {
      
      public static Signa of(Cell... cells) {
         var c = createEmpty();
         for (var cell : cells) c.setClue(cell);
         return new Signa(c);
      }
      public Signa deepCopyGrid() { return new Signa(new Clues(c.lo, c.hi, c.vlo, c.vhi, c.rlo, c.rhi, c.xlo, c.xhi)); }
      String gridToString() {
         var sb = new StringBuilder(INIT_GRID_OUTPUT);
         Masker.forEachSlot(c, (s, _, _) -> {
            val idx = Slot.clueIndex(s);
            val dir = Slot.dir(s);
            sb.setCharAt(INDEX(INDEX_ROW(idx), C + 1, INDEX_COL(idx)), CLUE_CHAR(dir));
         });
         return sb.toString();
      }
      public Stream<Vestigium> stream() {
         val stream = Stream.<Vestigium>builder();
         for (var l = c.lo & ~c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), RIGHT1.dir));
         for (var l = c.lo & ~c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), DOWN0.dir));
         for (var l = c.lo & ~c.xlo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_UP));
         for (var l = c.lo & ~c.xlo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_LEFT));
         for (var l = c.lo & c.xlo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_LEFT_TOP));
         for (var l = c.lo & c.xlo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new Vestigium(Long.numberOfTrailingZeros(l), CLUE_RIGHT_TOP));
         
         for (var h = c.hi & ~c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT1));
         for (var h = c.hi & ~c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_DOWN0));
         for (var h = c.hi & ~c.xhi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_UP));
         for (var h = c.hi & ~c.xhi & c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT));
         for (var h = c.hi & c.xhi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_LEFT_TOP));
         for (var h = c.hi & c.xhi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new Vestigium(HI(Long.numberOfTrailingZeros(h)), CLUE_RIGHT_TOP));
         
         return stream.build();
      }
      public Slotinfo[] slots(Dict D) {
         return Masker.slots(c, D.index(), D.reversed());
      }
   }
   
   record Puzzle(@Delegate Grid grid, Clues cl)
           implements Stream<Lettrix> {
      
      public Puzzle(Clues clues) { this(Masker.toGrid(clues), clues); }
      public Puzzle(Signa clues) { this(clues.c); }
      public @Delegate Stream<Lettrix> stream() {
         val stream = Stream.<Lettrix>builder();
         for (var l = grid.lo & ~cl.lo; l != X; l &= l - 1) stream.accept(Lettrix.from(Long.numberOfTrailingZeros(l), grid.g));
         for (var h = grid.hi & ~cl.hi & 0xFF; h != X; h &= h - 1) stream.accept(Lettrix.from(64 | Long.numberOfTrailingZeros(h), grid.g));
         return stream.build();
      }
      String gridToString() {
         var sb = INIT_GRID_OUTPUT_ARR.clone();
         Masker.forEachSlot(cl, (s, _, _) -> {
            val idx = Slot.clueIndex(s);
            val r   = idx & 7;
            val c   = idx >>> 3;
            val dir = Slot.dir(s);
            sb[r * (C + 1) + c] = (byte) (dir | 48);
         });
         stream().forEach((l) -> sb[l.index(C + 1)] = (byte) l.human());
         return new String(sb);
      }
      public String[] exportGrid(Replacar clueChar, char emptyFallback) {
         var sb = INIT_GRID_OUTPUT_ARR.clone();
         Masker.forEachSlot(cl, (s, l, a) -> {
            val idx = Slot.clueIndex(s);
            val r   = idx & 7;
            val c   = idx >>> 3;
            val dir = Slot.dir(s);
            sb[r * (C + 1) + c] = (byte) clueChar.replace(new Rell(grid, cl, idx, (byte) (dir | 48)));
         });
         stream().forEach((l) -> sb[l.index(C + 1)] = (byte) l.human());
         return new String(sb).replaceAll(" ", String.valueOf(emptyFallback)).split("\n");
      }
      public static IntStream cellWalk(byte base, long lo, long hi) {
         if (Slotinfo.increasing(base)) {
            return IntStream.concat(
                    IntStream.generate(new IntSupplier() {
                       
                       long temp = lo;
                       @Override
                       public int getAsInt() {
                          int res = Long.numberOfTrailingZeros(temp);
                          temp &= temp - 1;
                          return res;
                       }
                    }).limit(Long.bitCount(lo)),
                    IntStream.generate(new IntSupplier() {
                       
                       long temp = hi;
                       @Override
                       public int getAsInt() {
                          int res = 64 | Long.numberOfTrailingZeros(temp);
                          temp &= temp - 1;
                          return res;
                       }
                    }).limit(Long.bitCount(hi)));
         } else {
            return IntStream.concat(
                    IntStream.generate(new IntSupplier() {
                       
                       long temp = hi;
                       @Override
                       public int getAsInt() {
                          int msb = 63 - Long.numberOfLeadingZeros(temp);
                          temp &= ~(1L << msb);
                          return 64 | msb;
                       }
                    }).limit(Long.bitCount(hi)),
                    IntStream.generate(new IntSupplier() {
                       
                       long temp = lo;
                       @Override
                       public int getAsInt() {
                          int msb = 63 - Long.numberOfLeadingZeros(temp);
                          temp &= ~(1L << msb);
                          return msb;
                       }
                    }).limit(Long.bitCount(lo)));
         }
      }
      
      public String renderHuman() { return String.join("\n", exportGrid(_ -> ' ', '#')); }
      @FunctionalInterface
      interface Replacar {
         
         record Rell(Grid grid, Clues clues, int index, byte data) { }
         char replace(Rell c);
      }
      
   }
   
   record Placed(long lemma, int slotKey, int[] cells) {
      
      public static final char   HORIZONTAL = 'h';
      static final        char   VERTICAL   = 'v';
      static final        char[] DIRECTION  = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.VERTICAL };
      
      public int arrowCol()       { return Masker.IT[Slot.clueIndex(slotKey)].c(); }
      public int arrowRow()       { return Masker.IT[Slot.clueIndex(slotKey)].r(); }
      public int startRow()       { return Masker.IT[cells[0]].r(); }
      public int startCol()       { return Masker.IT[cells[0]].c(); }
      public boolean isReversed() { return !Slotinfo.increasing(slotKey); }
      public char direction()     { return DIRECTION[Slot.dir(slotKey)]; }
   }
   
   public record Rewards(int coins, int stars, int hints) { }
   
   public record WordOut(String word, int[] cell, int startRow, int startCol, char direction, int arrowRow, int arrowCol, boolean isReversed, int complex, String[] clue) {
      
      record ShaLemma(String word, @Delegate ShardLem rec) { }
      private static ShaLemma lookup(long w, byte[] bytes) {
         
         try {
            val rec = Meta.lookupSilent(w);
            System.out.println("\nQuery: w=" + w + " -> i=" + rec.mmap());
            var word1 = Lemma.asWord(w, bytes);
            System.out.println("  word=" + word1 + "\n" + "  simpel=" + rec.simpel() + "\n" + "  clues=" + Arrays.toString(rec.clues()));
            return new ShaLemma(word1, rec);
         } catch (Exception e) {
            throw new RuntimeException(e);
         }
      }
      static long reverse(long w) {
         int  L       = Lemma.unpackSize(w) + 1;
         long letters = w & Lemma.LETTER_MASK;
         long rev     = 0;
         for (int i = 0; i < L; i++) {
            long letter = (letters >>> (5 * i)) & 31;
            rev |= (letter << (5 * (L - 1 - i)));
         }
         return (w & ~Lemma.LETTER_MASK) | rev;
      }
      public WordOut(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed, byte[] bytes) {
         val meta = lookup(isReversed ? reverse(l) : l, bytes);
         this(meta.word, new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed,
              meta.simpel(), meta.clues());
      }
   }
   
   public record ExportedPuzzle(String[] grid, WordOut[] words, int difficulty, Rewards rewards) { }
   
   public record PuzzleResult(Signa clues, Puzzle grid, Slotinfo[] slots, FillResult filled) {
      
      public ExportedPuzzle exportFormatFromFilled(Rewards rewards) {
         // If nothing placed: return full grid mapped to letters/# only
         if (slots.length == 0) {
            return new ExportedPuzzle(grid.exportGrid(_ -> '#', '#'), new WordOut[0], 1, rewards);
         }
         
         var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Puzzle.cellWalk((byte) slot.key(), slot.lo(), slot.hi()).toArray())).toArray(
                 Placed[]::new);
         
         // 2) bounding box around all word cells + arrow cells, with 1-cell margin
         int minR = Integer.MAX_VALUE, minC = Integer.MAX_VALUE;
         int maxR = Integer.MIN_VALUE, maxC = Integer.MIN_VALUE;
         
         for (var rc : placed) {
            for (var c : rc.cells) {
               val it = Masker.IT[c];
               minR = Math.min(minR, it.r());
               minC = Math.min(minC, it.c());
               maxR = Math.max(maxR, it.r());
               maxC = Math.max(maxC, it.c());
            }
            minR = Math.min(minR, rc.arrowRow());
            minC = Math.min(minC, rc.arrowCol());
            maxR = Math.max(maxR, rc.arrowRow());
            maxC = Math.max(maxC, rc.arrowCol());
         }
         
         // 3) map of only used letter cells (everything else becomes '#')
         var map = grid.stream().collect(Collectors.toMap(Lettrix::index, Lettrix::human));
         // 4) render gridv2 over cropped bounds (out-of-bounds become '#')
         var gridv2 = new String[Math.max(0, maxR - minR + 1)];
         for (int r = minR, i = 0; r <= maxR; r++, i++) {
            var row = new StringBuilder(Math.max(0, maxC - minC + 1));
            for (var c = minC; c <= maxC; c++) row.append(map.getOrDefault(Masker.offset(r, c), '#'));
            gridv2[i] = row.toString();
         }
         
         // 5) words output with cropped coordinates
         int MIN_R = minR, MIN_C = minC;
         val bytes = BYTES.get();
         var wordsOut = Arrays.stream(placed).map(p -> new WordOut(
                 p.lemma,
                 p.startRow() - MIN_R,
                 p.startCol() - MIN_C,
                 p.direction(),
                 p.arrowRow() - MIN_R,
                 p.arrowCol() - MIN_C,
                 p.isReversed(), bytes
         )).toArray(WordOut[]::new);
         var total = 0.0001d + Arrays.stream(wordsOut).mapToDouble(WordOut::complex).sum();
         return new ExportedPuzzle(gridv2, wordsOut, (int) (total / wordsOut.length), rewards);
      }
   }
   
   record Lettrix(int index, byte letter) {
      
      public int row()                             { return INDEX_ROW(index); }
      public int col()                             { return INDEX_COL(index); }
      public char human()                          { return LETTER(letter); }
      static Lettrix from(int index, byte[] bytes) { return new Lettrix(index, bytes[index]); }
      public int index(int cols)                   { return (row() * cols) + col(); }
   }
}
