import 'dart:math' as math;

import 'package:flutter/material.dart';

import '../models/slide.dart';
import '../widgets/slides/inline_markdown.dart';

/// Reference slide width (960pt) used for consistent quality metrics across
/// deck sizes — matches the PowerPoint 16:9 canvas the previews emulate.
const double kReferenceSlideWidth = 960.0;

const double kBulletsMaxScale = 3.2;
const double kSplitBulletsMaxScale = 4.35;

/// Hard ceiling for rendered bullet text as a fraction of slide width.
const double kBulletMaxFontFraction = 0.0335;

const double kBulletLineHeight = 1.16;

/// Text-density warning when auto-fit shrinks below this scale factor.
const double kTextDensityWarningScale = 0.70;

/// Matches the minimum scale passed to [bulletsFitScale] in previews.
const double kTextDensityCriticalScale = 0.20;

/// The largest auto-fit scale that keeps bullets at or under
/// [kBulletMaxFontFraction], given the layout's own [layoutMax] growth bound.
double bulletScaleCap(double w, double bulletSize, double layoutMax) =>
    math.min(layoutMax, kBulletMaxFontFraction * w / bulletSize);

String bulletListMarker(List<String> items, int index, ListStyle style) {
  int levelOf(String item) {
    var level = 0;
    while (level < item.length && item[level] == '\t') {
      level++;
    }
    return level;
  }

  final level = levelOf(items[index]);
  if (style == ListStyle.bullets) return _bulletMarkerForLevel(level);
  if (style == ListStyle.checklist) {
    return checklistItemChecked(items[index]) ? '☑' : '☐';
  }
  var number = 0;
  for (var i = 0; i <= index; i++) {
    final itemLevel = levelOf(items[i]);
    if (itemLevel == level) number++;
    if (itemLevel < level) number = 0;
  }
  return '$number.';
}

String _bulletMarkerForLevel(int level) {
  const markers = ['•', '◦', '▪', '▫', '–'];
  return markers[level.clamp(0, markers.length - 1)];
}

double bulletLevelScale(int level) {
  if (level <= 0) return 1.0;
  if (level == 1) return 0.86;
  if (level == 2) return 0.80;
  return 0.76;
}

/// Largest scale in [minScale, maxScale] for which the bullet block fits
/// [availH] at the full column width.
double bulletsFitScale({
  required double availW,
  required double availH,
  required bool hasTitle,
  required String title,
  required List<String> bullets,
  required double titleSize,
  required double bulletSize,
  required double spacing,
  required double bulletGap,
  required String font,
  String subtitle = '',
  double subtitleSize = 0,
  double minScale = kTextDensityCriticalScale,
  double maxScale = 1.0,
  ListStyle listStyle = ListStyle.bullets,
}) {
  if (availW <= 0 || !availH.isFinite || availH <= 0) return 1.0;
  final budget = availH * 0.98;
  double measure(double scale) => bulletsBlockHeight(
    scale: scale,
    availW: availW,
    listStyle: listStyle,
    hasTitle: hasTitle,
    title: title,
    bullets: bullets,
    titleSize: titleSize,
    bulletSize: bulletSize,
    spacing: spacing,
    bulletGap: bulletGap,
    font: font,
    subtitle: subtitle,
    subtitleSize: subtitleSize,
  );

  if (measure(maxScale) <= budget) return maxScale;

  double lo, hi;
  if (maxScale > 1.0 && measure(1.0) <= budget) {
    lo = 1.0;
    hi = maxScale;
  } else {
    lo = minScale;
    hi = maxScale > 1.0 ? 1.0 : maxScale;
  }
  for (var i = 0; i < 24; i++) {
    final mid = (lo + hi) / 2;
    if (measure(mid) <= budget) {
      lo = mid;
    } else {
      hi = mid;
    }
  }
  return lo;
}

double bulletsBlockHeight({
  required double scale,
  required double availW,
  required bool hasTitle,
  required String title,
  required List<String> bullets,
  required double titleSize,
  required double bulletSize,
  required double spacing,
  required double bulletGap,
  required String font,
  String subtitle = '',
  double subtitleSize = 0,
  ListStyle listStyle = ListStyle.bullets,
}) {
  var height = 0.0;
  if (hasTitle) {
    height += measureTextHeight(
      title,
      titleSize * scale,
      availW,
      bold: true,
      fontFamily: font,
    );
  }
  if (subtitle.isNotEmpty) {
    height += spacing * scale * 0.4;
    height += measureTextHeight(
      subtitle,
      subtitleSize * scale,
      availW,
      bold: true,
      fontFamily: font,
    );
  }
  if ((hasTitle || subtitle.isNotEmpty) && bullets.isNotEmpty) {
    height += spacing * scale;
  }
  for (var i = 0; i < bullets.length; i++) {
    final b = bullets[i];
    final level = bulletLevel(b);
    final text = listStyle == ListStyle.checklist
        ? checklistItemText(b)
        : b.substring(level);
    final fontSize = bulletSize * bulletLevelScale(level) * scale;
    final indent = level * bulletSize * 1.05 * scale;
    final marker = '${bulletListMarker(bullets, i, listStyle)} ';
    final markerW = measureTextWidth(
      marker,
      fontSize,
      bold: true,
      fontFamily: font,
    );
    final wrapW = (availW - indent - markerW).clamp(1.0, availW);
    final textH = measureTextHeight(
      text,
      fontSize,
      wrapW,
      lineHeight: kBulletLineHeight,
      fontFamily: font,
    );
    final markerH = measureTextHeight(
      marker,
      fontSize,
      double.infinity,
      fontFamily: font,
    );
    height += bulletGap * scale * 2 + (textH > markerH ? textH : markerH);
  }
  return height;
}

double measureTextHeight(
  String text,
  double fontSize,
  double maxWidth, {
  double? lineHeight,
  bool bold = false,
  String? fontFamily,
}) {
  final painter = TextPainter(
    text: TextSpan(
      text: stripInlineMarkdown(text),
      style: TextStyle(
        fontFamily: fontFamily,
        fontSize: fontSize,
        height: lineHeight,
        fontWeight: bold ? FontWeight.bold : null,
      ),
    ),
    textDirection: TextDirection.ltr,
  )..layout(maxWidth: maxWidth.isFinite ? maxWidth : double.infinity);
  return painter.height;
}

double measureTextWidth(
  String text,
  double fontSize, {
  bool bold = false,
  String? fontFamily,
}) {
  final painter = TextPainter(
    text: TextSpan(
      text: stripInlineMarkdown(text),
      style: TextStyle(
        fontFamily: fontFamily,
        fontSize: fontSize,
        fontWeight: bold ? FontWeight.bold : null,
      ),
    ),
    textDirection: TextDirection.ltr,
  )..layout();
  return painter.width;
}

/// Table cell font size fraction used in [table_preview.dart].
double tableCellFontSize(double w, {required int rowCount, required int colCount}) {
  final density = (rowCount + colCount).clamp(2, 24);
  return (w * 0.025 * (10 / (density + 6))).clamp(w * 0.010, w * 0.021);
}

/// Minimum table cell font fraction (lower clamp bound in previews).
double tableCellFontMinimum(double w) => w * 0.010;

/// Layout metrics for a standard bullets slide at [kReferenceSlideWidth].
double bulletsSlideFitScale({
  required Slide slide,
  required String font,
  bool includeChecklistProgress = true,
}) {
  final w = kReferenceSlideWidth;
  final pad = w * 0.07;
  final vPad = w * 0.05;
  final titleSize = w * 0.042;
  final subtitleSize = w * 0.030;
  final bulletSize = w * 0.026;
  final spacing = pad * 0.5;
  final bulletGap = w * 0.006;
  final bullets = slide.bullets.where((b) => b.trimLeft().isNotEmpty).toList();
  final showProgress =
      includeChecklistProgress &&
      slide.listStyle == ListStyle.checklist &&
      slide.showChecklistProgress &&
      bullets.isNotEmpty;

  final slideHeight = w * 9 / 16;
  final availW = (w - pad * 2).clamp(w * 0.12, w);
  final progressGap = w * 0.025;
  final progressW = w * 0.34;
  final textAvailW = showProgress
      ? (availW - progressGap - progressW).clamp(w * 0.12, availW)
      : availW;
  final availH = slideHeight - vPad * 2;

  return bulletsFitScale(
    availW: textAvailW,
    availH: availH,
    hasTitle: slide.title.isNotEmpty,
    title: slide.title,
    bullets: bullets,
    titleSize: titleSize,
    bulletSize: bulletSize,
    spacing: spacing,
    bulletGap: bulletGap,
    font: font,
    subtitle: slide.subtitle,
    subtitleSize: subtitleSize,
    maxScale: bulletScaleCap(w, bulletSize, kSplitBulletsMaxScale),
    listStyle: slide.listStyle,
  );
}

/// Layout metrics for a two-column bullets slide.
double twoBulletsSlideFitScale({required Slide slide, required String font}) {
  final w = kReferenceSlideWidth;
  final pad = w * 0.07;
  final vPad = w * 0.05;
  final leftBullets = slide.bullets.where((b) => b.trimLeft().isNotEmpty).toList();
  final rightBullets = slide.bullets2
      .where((b) => b.trimLeft().isNotEmpty)
      .toList();
  final dense = math.max(leftBullets.length, rightBullets.length) > 12;
  final titleSize = w * (dense ? 0.034 : 0.04);
  final bulletSize = w * 0.024;
  final spacing = pad * (dense ? 0.28 : 0.38);
  final bulletGap = w * (dense ? 0.0036 : 0.0055);
  final columnGap = w * 0.055;
  final col1Title = slide.columnTitle1.trim();
  final col2Title = slide.columnTitle2.trim();
  final hasColumnTitles = col1Title.isNotEmpty || col2Title.isNotEmpty;
  final headingSize = w * (dense ? 0.023 : 0.03);
  final headingGap = w * (dense ? 0.007 : 0.012);

  final slideHeight = w * 9 / 16;
  final contentW = (w - pad * 2).clamp(w * 0.12, w);
  final columnW = ((contentW - columnGap) / 2).clamp(w * 0.12, w);
  var availH = slideHeight - vPad * 2;
  if (slide.title.isNotEmpty) {
    availH -= measureTextHeight(
      slide.title,
      titleSize,
      contentW,
      bold: true,
      fontFamily: font,
    );
    availH -= spacing;
  }
  double headingHeight(String t) => t.isEmpty
      ? 0
      : measureTextHeight(
          t,
          headingSize,
          columnW,
          bold: true,
          fontFamily: font,
        );
  final maxHeadingH = math.max(
    headingHeight(col1Title),
    headingHeight(col2Title),
  );
  if (hasColumnTitles) availH -= maxHeadingH + headingGap;

  final leftScale = bulletsFitScale(
    availW: columnW,
    availH: availH,
    hasTitle: false,
    title: '',
    bullets: leftBullets,
    titleSize: titleSize,
    bulletSize: bulletSize,
    spacing: spacing,
    bulletGap: bulletGap,
    font: font,
    maxScale: bulletScaleCap(w, bulletSize, kBulletsMaxScale),
    listStyle: slide.listStyle,
  );
  final rightScale = bulletsFitScale(
    availW: columnW,
    availH: availH,
    hasTitle: false,
    title: '',
    bullets: rightBullets,
    titleSize: titleSize,
    bulletSize: bulletSize,
    spacing: spacing,
    bulletGap: bulletGap,
    font: font,
    maxScale: bulletScaleCap(w, bulletSize, kBulletsMaxScale),
    listStyle: slide.listStyle,
  );
  return math.min(leftScale, rightScale);
}

/// Layout metrics for a bullets + image slide.
double bulletsImageSlideFitScale({required Slide slide, required String font}) {
  final w = kReferenceSlideWidth;
  final leftPad = w * 0.038;
  final verticalPad = w * 0.042;
  final gap = leftPad;
  final imgFraction = (slide.imageSize > 0 ? slide.imageSize / 100.0 : 0.40)
      .clamp(0.1, 0.70);
  final imgWidth = w * imgFraction;
  final bulletSize = w * 0.031;
  final titleSize = w * 0.042;
  final spacing = verticalPad * 0.32;
  final bulletGap = w * 0.005;
  final bullets = slide.bullets.where((b) => b.trimLeft().isNotEmpty).toList();

  final slideHeight = w * 9 / 16;
  final availW = (w - imgWidth - gap - leftPad).clamp(w * 0.12, w);
  final availH = slideHeight - verticalPad * 2;

  return bulletsFitScale(
    availW: availW,
    availH: availH,
    hasTitle: slide.title.isNotEmpty,
    title: slide.title,
    bullets: bullets,
    titleSize: titleSize,
    bulletSize: bulletSize,
    spacing: spacing,
    bulletGap: bulletGap,
    font: font,
    maxScale: bulletScaleCap(w, bulletSize, kBulletsMaxScale),
    listStyle: slide.listStyle,
  );
}
