package puzzle;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.Delegate;
import lombok.val;
import puzzle.Export.Gridded.Replacar.Cell;
import puzzle.Export.LetterVisit.LetterAt;
import puzzle.Masker.Clues;
import puzzle.SwedishGenerator.Dict;
import puzzle.SwedishGenerator.DictEntry;
import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Grid;
import puzzle.SwedishGenerator.Slotinfo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.function.IntSupplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static puzzle.SwedishGenerator.R;
import static puzzle.SwedishGenerator.Lemma;
import static puzzle.Masker.Slot;
import static puzzle.SwedishGenerator.C;
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() {
   
   record Strings() {
      
      static String padRight(String s, int n) {
         if (s.length() >= n) return s;
         return s + " ".repeat(n - s.length());
      }
   }
   
   static final String INIT = IntStream.range(0, R).mapToObj(l_ -> "         ").collect(Collectors.joining("\n"));
   
   public record ClueAt(int index, int clue) { }
   
   public record Clued(@Delegate Clues c) {
      
      public Clued deepCopyGrid() { return new Clued(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);
         forEachSlot((s, _, _) -> {
            val idx = Slot.clueIndex(s);
            val r   = idx & 7;
            val c   = idx >>> 3;
            val dir = Slot.dir(s);
            sb.setCharAt(r * (C + 1) + c, (char) (dir | 48));
         });
         return sb.toString();
      }
      public Stream<ClueAt> stream() {
         val stream = Stream.<ClueAt>builder();
         for (var l = c.lo & ~c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 1));
         for (var l = c.lo & ~c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 0));
         for (var l = c.lo & c.rlo & ~c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 2));
         for (var l = c.lo & c.rlo & c.vlo; l != X; l &= l - 1) stream.accept(new ClueAt(Long.numberOfTrailingZeros(l), 3));
         for (var h = c.hi & ~c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(64 | Long.numberOfTrailingZeros(h), 1));
         for (var h = c.hi & ~c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt(64 | Long.numberOfTrailingZeros(h), 0));
         for (var h = c.hi & c.rhi & ~c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt((64 | Long.numberOfTrailingZeros(h)), 2));
         for (var h = c.hi & c.rhi & c.vhi; h != X; h &= h - 1) stream.accept(new ClueAt((64 | Long.numberOfTrailingZeros(h)), 3));
         
         return stream.build();
      }
   }
   
   @FunctionalInterface
   interface LetterVisit {
      
      record LetterAt(int index, byte letter) {
         
         public char human() {
            return (char) (letter | 64);
         }
         static LetterAt from(int index, byte[] bytes) { return new LetterAt(index, bytes[index]); }
      }
      
      void visit(int index, byte letter);
      default void visit(int index, byte[] letters) { visit(index, letters[index]); }
   }
   
   record Gridded(@Delegate Grid grid) {
      
      public Stream<LetterAt> stream(Clues clues) {
         val stream = Stream.<LetterAt>builder();
         for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) stream.accept(LetterAt.from(Long.numberOfTrailingZeros(l), grid.g));
         for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) stream.accept(LetterAt.from(64 | Long.numberOfTrailingZeros(h), grid.g));
         return stream.build();
      }
      public void forEachLetter(Clues clues, LetterVisit visitor) {
         for (var l = grid.lo & ~clues.lo; l != X; l &= l - 1) visitor.visit(Long.numberOfTrailingZeros(l), grid.g);
         for (var h = grid.hi & ~clues.hi & 0xFF; h != X; h &= h - 1) visitor.visit(64 | Long.numberOfTrailingZeros(h), grid.g);
      }
      public static IntStream walk(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);
                          int res = 64 | msb;
                          temp &= ~(1L << msb);
                          return res;
                       }
                    }).limit(Long.bitCount(hi)),
                    IntStream.generate(new IntSupplier() {
                       
                       long temp = lo;
                       @Override
                       public int getAsInt() {
                          int msb = 63 - Long.numberOfLeadingZeros(temp);
                          int res = msb;
                          temp &= ~(1L << msb);
                          return res;
                       }
                    }).limit(Long.bitCount(lo)));
         }
      }
      String gridToString(Clues clues) {
         var sb = new StringBuilder(INIT);
         clues.forEachSlot((s, _, _) -> {
            val idx = Slot.clueIndex(s);
            val r   = idx & 7;
            val c   = idx >>> 3;
            val dir = Slot.dir(s);
            sb.setCharAt(r * (C + 1) + c, (char) (dir | 48));
         });
         forEachLetter(clues, (idx, letter) -> {
            val r = idx & 7;
            val c = idx >>> 3;
            sb.setCharAt(r * (C + 1) + c, (char) (letter | 64));
         });
         return sb.toString();
      }
      public String renderHuman(Clues clues) {
         return String.join("\n", exportGrid(clues, _ -> ' ', '#'));
      }
      @FunctionalInterface
      interface Replacar {
         
         record Cell(Grid grid, Clues clues, int index, byte data) { }
         char replace(Cell c);
      }
      public String[] exportGrid(Clues clues, Replacar clueChar, char emptyFallback) {
         var sb = new StringBuilder(INIT);
         clues.forEachSlot((s, l, a) -> {
            val idx = Slot.clueIndex(s);
            val r   = idx & 7;
            val c   = idx >>> 3;
            val dir = Slot.dir(s);
            sb.setCharAt(r * (C + 1) + c, clueChar.replace(new Cell(grid, clues, idx, (byte) (dir | 48))));
         });
         forEachLetter(clues, (idx, letter) -> {
            val r = idx & 7;
            val c = idx >>> 3;
            sb.setCharAt(r * (C + 1) + c, (char) (letter | 64));
         });
         return sb.toString().replaceAll(" ", "" + emptyFallback).split("\n");
      }
   }
   
   record Placed(long lemma, int slotKey, int[] cells) {
      
      static final        char[] DIRECTION  = { Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL, Placed.HORIZONTAL, Placed.VERTICAL };
      public static final char   HORIZONTAL = 'h';
      static final        char   VERTICAL   = 'v';
      public int arrowCol()       { return SwedishGenerator.IT[Slot.clueIndex(slotKey)].c(); }
      public int arrowRow()       { return SwedishGenerator.IT[Slot.clueIndex(slotKey)].r(); }
      public int startRow()       { return SwedishGenerator.IT[cells[0]].r(); }
      public int startCol()       { return SwedishGenerator.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) {
      
      public WordOut(long l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed) {
         val meta = Meta.readRecord(Meta.shardKey(l), Lemma.unpackShardIndex(l));
         this(Lemma.asWord(l), 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(Clued clues, Gridded grid, Slotinfo[] slots, FillResult filled) {
      
      static public long calcSimpel(Slotinfo[] slots) {
         int  k      = 0;
         long simpel = 0L;
         for (var n = 1; n < slots.length; n++) {
            if (slots[n].assign().w != X) {
               k++;
               simpel += Meta.readRecord(Meta.shardKey(slots[n].assign().w), Lemma.unpackShardIndex(slots[n].assign().w)).simpel();
            }
         }
         simpel = k == 0 ? 0 : simpel / k;
         return simpel;
      }
      public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
         // If nothing placed: return full grid mapped to letters/# only
         if (slots.length == 0) {
            return new ExportedPuzzle(grid.exportGrid(clues.c, _ -> '#', '#'), new WordOut[0], difficulty, rewards);
         }
         
         var placed = Arrays.stream(slots).map(slot -> new Placed(slot.assign().w, slot.key(), Gridded.walk((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 = SwedishGenerator.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(clues.c()).collect(Collectors.toMap(LetterAt::index, LetterAt::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(Grid.offset(r, c), '#'));
            gridv2[i] = row.toString();
         }
         
         // 5) words output with cropped coordinates
         int MIN_R = minR, MIN_C = minC;
         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()
         )).toArray(WordOut[]::new);
         return new ExportedPuzzle(gridv2, wordsOut, difficulty, rewards);
      }
   }
   
   interface Dicts {
      
      static Dict makeDict(long[] wordz) {
         var index = new DictEntryDTO[SwedishGenerator.MAX_WORD_LENGTH_PLUS_ONE];
         Arrays.setAll(index, DictEntryDTO::new);
         for (var lemma : wordz) {
            var L     = Lemma.unpackSize(lemma) + 1;//Lemma.unpackSize(lemma) + 2;
            val entry = index[L];
            val idx   = entry.words().size();
            val pos   = entry.pos();
            entry.words().add(lemma);
            int i = 0;
            for (long w = lemma & Lemma.LETTER_MASK; w != 0; w >>>= 5, i++) {
               pos[i][(int) ((w & 31) - 1)].add(idx);
            }
            // for (i = 0; i < L; i++) entry.pos()[i][Lemma.byteAt(lemma, i) - 1].add(idx);
         }
         for (int i = 2; i < index.length; i++) if (index[i].words().size() <= 0) throw new RuntimeException("No words for length " + i);
         return new Dict(Arrays.stream(index).map(i -> {
            var words    = i.words().toArray();
            int numWords = words.length;
            int numLongs = (numWords + 63) >>> 6;
            var bitsets  = new long[i.pos().length * 26][numLongs];
            for (int p = 0; p < i.pos().length; p++) {
               for (int l = 0; l < 26; l++) {
                  var list = i.pos()[p][l];
                  var bs   = bitsets[p * 26 + l];
                  for (int k = 0; k < list.size(); k++) {
                     int wordIdx = list.data()[k];
                     bs[wordIdx >>> 6] |= (1L << (wordIdx & 63));
                  }
               }
            }
            return new DictEntry(words, bitsets, words.length, (words.length + 63) >>> 6);
         }).toArray(DictEntry[]::new),
                         Arrays.stream(index).mapToInt(i -> i.words().size()).sum());
      }
   }
   
   record DictEntryDTO(LongArrayList words, IntListDTO[][] pos) {
      
      public DictEntryDTO(int L) {
         this(new LongArrayList(1024), new IntListDTO[L][26]);
         for (var i = 0; i < L; i++) for (var j = 0; j < 26; j++) pos[i][j] = new IntListDTO();
      }
   }
   
   @Getter
   @Accessors(fluent = true)
   @NoArgsConstructor
   static final class IntListDTO {
      
      int[] data = new int[8];
      int   size = 0;
      public IntListDTO(int size) {
         data = new int[size];
      }
      void add(int v) {
         if (size >= data.length) data = Arrays.copyOf(data, data.length * 2);
         data[size++] = v;
      }
      int[] toArray() { return Arrays.copyOf(data, size); }
   }
}
