package puzzle;

import lombok.experimental.Delegate;
import puzzle.SwedishGenerator.Dict;
import puzzle.SwedishGenerator.FillResult;
import puzzle.SwedishGenerator.Grid;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import static java.nio.charset.StandardCharsets.*;
import static puzzle.SwedishGenerator.R;
import static puzzle.SwedishGenerator.Lemma;
import static puzzle.SwedishGenerator.Slot;
import static puzzle.SwedishGenerator.C;

/**
 * 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());
      }
   }
   
   record Gridded(@Delegate Grid grid) {
      
      String gridToString() {
         var sb = new StringBuilder();
         for (var r = 0; r < R; r++) {
            if (r > 0) sb.append('\n');
            for (var c = 0; c < C; c++) sb.append((char) grid.byteAt(Grid.offset(r, c)));
         }
         return sb.toString();
      }
      public String renderHuman() {
         var sb = new StringBuilder();
         for (var r = 0; r < R; r++) {
            if (r > 0) sb.append('\n');
            for (var c = 0; c < C; c++) {
               sb.append(grid.isDigitAt(Grid.offset(r, c)) ? ' ' : (char) grid.byteAt(Grid.offset(r, c)));
            }
         }
         return sb.toString();
      }
   }
   
   static class Bit {
      
      static long pack(int r, int c) { return (((long) r) << 32) ^ (c & 0xFFFFFFFFL); }
      long l1, l2;
      public boolean get(int bitIndex) {
         if (bitIndex < 64) return (l1 & (1L << bitIndex)) != 0L;
         return (l2 & (1L << (bitIndex & 63))) != 0L;
      }
      public void set(int bitIndex) {
         if (bitIndex < 64) this.l1 |= 1L << bitIndex;
         else this.l2 |= 1L << (bitIndex & 63);
      }
      public void clear() {
         l1 = 0L;
         l2 = 0L;
      }
   }
   
   record Bit1029(long[] bits) {
      
      public Bit1029()                   { this(new long[2048]); }
      static int wordIndex(int bitIndex) { return bitIndex >> 6; }
      public boolean get(int bitIndex)   { return (this.bits[wordIndex(bitIndex)] & 1L << bitIndex) != 0L; }
      public void set(int bitIndex)      { bits[wordIndex(bitIndex)] |= 1L << bitIndex; }
      public void clear(int bitIndex)    { this.bits[wordIndex(bitIndex)] &= ~(1L << bitIndex); }
      public void clear()                { Arrays.fill(bits, 0L); }
   }
   
     record Placed(Lemma lemma, int startRow, int startCol, char direction, int arrowRow, int arrowCol, int[] cells, boolean isReversed) {
      
      public static final char HORIZONTAL = 'h';
      static final char VERTICAL   = 'v';
   }
   
   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(Lemma l, int startRow, int startCol, char d, int arrowRow, int arrowCol, boolean isReversed) {
         this(new String(l.word(), US_ASCII), new int[]{ arrowRow, arrowCol, startRow, startCol }, startRow, startCol, d, arrowRow, arrowCol, isReversed, l.simpel(), l.clue());
      }
   }
   
   public record ExportedPuzzle(String[] gridv2, WordOut[] words, int difficulty, Rewards rewards) { }
   
   public record PuzzleResult(SwedishGenerator swe, Dict dict, Gridded mask, FillResult filled) {
      
      boolean inBounds(int r, int c) { return r >= 0 && r < SwedishGenerator.R && c >= 0 && c < SwedishGenerator.C; }
      Placed extractPlacedFromSlot(Slot s, Lemma lemma) {
         var d = s.dir();
         
         var cells = new int[s.len()];
         for (int i = 0, len = s.len(); i < len; i++) cells[i] = s.pos(i);
         
         char direction;
         var    isReversed = false;
         var    startRow   = Grid.r(cells[0]);
         var    startCol   = Grid.c(cells[0]);
         if (d == 2) {              // right -> horizontal
            direction = Placed.HORIZONTAL;
         } else if (d == 3 || d == 5) {       // down or down-bent -> vertical
            direction = Placed.VERTICAL;
         } else if (d == 4) {       // left -> horizontal (REVERSED)
            direction  = Placed.HORIZONTAL;
            isReversed = true;
         } else if (d == 1) {       // up -> vertical (REVERSED)
            direction  = Placed.VERTICAL;
            isReversed = true;
         } else {
            return null;
         }
         
         return new Placed(
                 lemma,
                 startRow,
                 startCol,
                 direction,
                 s.clueR(),
                 s.clueC(),
                 cells,
                 isReversed
         );
      }
      public ExportedPuzzle exportFormatFromFilled(int difficulty, Rewards rewards) {
         var g       = filled().grid();
         var placed  = new ArrayList<Placed>();
         var clueMap = filled().clueMap();
         g.grid().forEachSlot((int key, long packedPos, int len) -> {
            var word = clueMap.get(key);
            if (word != null) {
               var p = extractPlacedFromSlot(Slot.from(key, packedPos, len), word);
               if (p != null) placed.add(p);
            }
         });
         
         // If nothing placed: return full grid mapped to letters/# only
         if (placed.isEmpty()) {
            var gridv2 = new String[R];
            for (var r = 0; r < R; r++) {
               var sb = new StringBuilder(C);
               for (var c = 0; c < C; c++) {
                  int idx = Grid.offset(r, c);
                  sb.append(g.isLetterSet(idx) ? (char) g.byteAt(idx) : '#');
               }
               gridv2[r] = sb.toString();
            }
            return new ExportedPuzzle(gridv2, new WordOut[0], difficulty, rewards);
         }
         
         // 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) {
               minR = Math.min(minR, Grid.r(c));
               minC = Math.min(minC, Grid.c(c));
               maxR = Math.max(maxR, Grid.r(c));
               maxC = Math.max(maxC, Grid.c(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 letterAt = new HashMap<Long, Character>();
         for (var p : placed) {
            for (var c : p.cells) {
               int rr  = Grid.r(c), cc = Grid.c(c);
               int idx = Grid.offset(rr, cc);
               if (inBounds(rr, cc) && g.isLetterSet(idx)) {
                  letterAt.put(Bit.pack(rr, cc), (char) g.byteAt(idx));
               }
            }
         }
         
         // 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(letterAt.getOrDefault(Bit.pack(r, c), '#'));
            gridv2[i] = row.toString();
         }
         
         // 5) words output with cropped coordinates
         int MIN_R = minR, MIN_C = minC;
         var wordsOut = placed.stream().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);
      }
   }
}
