/// Pure-Dart CTC (Connectionist Temporal Classification) greedy decoder.
///
/// Decodes per-timestep class logits into a string by taking the argmax at
/// each timestep, collapsing consecutive duplicate classes, dropping the
/// blank class, and mapping the remaining class indices to characters.
///
/// Index mapping note (PaddleOCR PP-OCR rec convention with [blankIndex] == 0):
/// the CTC blank occupies class index 0, so the character dictionary is
/// shifted by one. The character for class index `k` (k >= 1) is
/// `charset[k - 1]`. If [blankIndex] != 0, this exact shift may not apply and
/// the mapping should be reviewed for the specific exported model.
class CtcDecoder {
  CtcDecoder(this.charset, {this.blankIndex = 0});

  /// The character dictionary (without the blank entry).
  final List<String> charset;

  /// The class index reserved for the CTC blank symbol.
  final int blankIndex;

  /// Decode `[T][C]` logits into a string.
  ///
  /// For each timestep the argmax over the `C` classes is taken; consecutive
  /// duplicate indices are collapsed and the blank index is dropped. Remaining
  /// indices are mapped to characters via the dictionary shift described in the
  /// class docs. Out-of-range indices are skipped.
  String decode(List<List<double>> logits) {
    final buffer = StringBuffer();
    var previousIndex = -1;

    for (final row in logits) {
      if (row.isEmpty) {
        previousIndex = -1;
        continue;
      }

      // argmax over the classes of this timestep.
      var bestIndex = 0;
      var bestValue = row[0];
      for (var c = 1; c < row.length; c++) {
        if (row[c] > bestValue) {
          bestValue = row[c];
          bestIndex = c;
        }
      }

      // Collapse consecutive duplicates.
      if (bestIndex == previousIndex) {
        continue;
      }
      previousIndex = bestIndex;

      // Drop the blank class.
      if (bestIndex == blankIndex) {
        continue;
      }

      final ch = _charForIndex(bestIndex);
      if (ch != null) {
        buffer.write(ch);
      }
    }

    return buffer.toString();
  }

  /// Reshape a flat row-major `[T*C]` list into `[T][C]` and decode it.
  String decodeFlat(List<double> flat, int timeSteps, int numClasses) {
    if (timeSteps <= 0 || numClasses <= 0) return '';
    final logits = <List<double>>[];
    for (var t = 0; t < timeSteps; t++) {
      final start = t * numClasses;
      final end = start + numClasses;
      if (end > flat.length) break;
      logits.add(flat.sublist(start, end));
    }
    return decode(logits);
  }

  /// Map a class index to its character, applying the blank shift. Returns null
  /// for the blank index or out-of-range indices.
  String? _charForIndex(int index) {
    if (index == blankIndex) return null;

    // With blankIndex == 0 the dictionary is shifted by one: class index k
    // maps to charset[k - 1]. For other blank positions we fall back to a
    // direct index, which may need adjustment per the exported model.
    final mapped = blankIndex == 0 ? index - 1 : index;
    if (mapped < 0 || mapped >= charset.length) return null;
    return charset[mapped];
  }
}
