import { normalizeTextBlocks } from "./textBlocks.js";
import { clampCanvasItem } from "./pictureCanvas.js";
import { ROW_PICTURE_EDIT_MIN_HEIGHT_PX } from "./documentPageMetrics.js";
import { resolvePreviewRowHeightsPx } from "./pageContentRows.js";

/** @typedef {{ spans: number[], skip: boolean[] }} PictureRowSpanLayout */

/**
 * Build rowSpan/skip layout from per-block "merge with next row" flags.
 * @param {number} rowCount
 * @param {boolean[]} mergeNextByRow mergeNextByRow[i] = block (i+1) merges down
 * @returns {PictureRowSpanLayout}
 */
export function computePictureRowSpans(rowCount, mergeNextByRow = []) {
  const spans = Array(rowCount).fill(1);
  const skip = Array(rowCount).fill(false);

  let i = 0;
  while (i < rowCount) {
    let span = 1;
    while (i + span < rowCount && mergeNextByRow[i + span - 1]) {
      span += 1;
    }
    spans[i] = span;
    for (let j = 1; j < span; j += 1) {
      skip[i + j] = true;
    }
    i += span;
  }

  return { spans, skip };
}

/** Read merge-next flags aligned to text block order (1-based block_index). */
export function pictureMergeNextFlags(pageForm, rowCount) {
  const blocks = normalizeTextBlocks(pageForm);
  const fromMap = pageForm?.picture_merge_next;
  return Array.from({ length: rowCount }, (_, rowIndex) => {
    const block = blocks[rowIndex];
    const key = String(block?.block_index ?? rowIndex + 1);
    if (fromMap && typeof fromMap === "object" && key in fromMap) {
      return Boolean(fromMap[key]);
    }
    return Boolean(block?.pic_merge_next);
  });
}

export function pictureMergeNextMap(pageForm, rowCount) {
  const flags = pictureMergeNextFlags(pageForm, rowCount);
  const blocks = normalizeTextBlocks(pageForm);
  const out = {};
  flags.forEach((flag, rowIndex) => {
    const key = String(blocks[rowIndex]?.block_index ?? rowIndex + 1);
    out[key] = flag;
  });
  return out;
}

export function setPictureMergeNext(pageForm, blockIndex, mergeNext) {
  const key = String(blockIndex);
  const rowCount = normalizeTextBlocks(pageForm).length;
  const nextMergeMap = {
    ...(pageForm.picture_merge_next || {}),
    [key]: Boolean(mergeNext),
  };
  const flags = pictureMergeNextFlags(
    { ...pageForm, picture_merge_next: nextMergeMap },
    rowCount
  );
  const layout = computePictureRowSpans(rowCount, flags);
  const startRowIndex = blockIndex - 1;
  const rowSpan = layout.spans[startRowIndex] || 1;
  const rowHeightsPx = rowHeightsPxFromForm(
    { ...pageForm, picture_merge_next: nextMergeMap },
    rowCount
  );

  const canvasItems = (pageForm.picture_canvas || []).map((item) => {
    if (Number(item.row_index ?? item.block_index) !== blockIndex) return item;
    return toPictureCellLocal(item, startRowIndex, rowSpan, rowHeightsPx);
  });

  return {
    ...pageForm,
    picture_merge_next: nextMergeMap,
    text_blocks: normalizeTextBlocks(pageForm).map((block) =>
      String(block.block_index) === key
        ? { ...block, pic_merge_next: Boolean(mergeNext) }
        : block
    ),
    picture_canvas: canvasItems,
  };
}

function rowHeightsPxFromForm(pageForm, rowCount) {
  const blocks = normalizeTextBlocks(pageForm);
  return resolvePreviewRowHeightsPx(
    blocks.length ? blocks : Array.from({ length: rowCount }, () => ({}))
  );
}

/** Cumulative row band starts/heights as % of full page body (0–100). */
export function rowBandLayout(rowHeightsPx) {
  const heights = rowHeightsPx.map((h) => Math.max(0, Number(h) || 0));
  const total = heights.reduce((sum, h) => sum + h, 0) || 1;
  const starts = [];
  const bandHeights = [];
  let cum = 0;
  for (const h of heights) {
    starts.push(cum);
    const pct = (h / total) * 100;
    bandHeights.push(pct);
    cum += pct;
  }
  return { starts, bandHeights, total };
}

function itemRowIndex(item, rowHeightsPx) {
  const explicit = Number(item?.row_index ?? item?.block_index);
  if (Number.isFinite(explicit) && explicit >= 1) {
    return Math.min(rowHeightsPx.length, Math.trunc(explicit));
  }

  const { starts, bandHeights } = rowBandLayout(rowHeightsPx);
  const centerY = Number(item?.y_pct) + Number(item?.h_pct) / 2;
  for (let i = 0; i < bandHeights.length; i += 1) {
    if (centerY < starts[i] + bandHeights[i] + 0.001) {
      return i + 1;
    }
  }
  return rowHeightsPx.length || 1;
}

/** Assign row_index; convert legacy page-level geometry to row-local %. */
export function normalizeItemsToRows(items, rowHeightsPx) {
  return (items || []).map((item) => {
    const hasExplicitRow =
      item.row_index != null || item.block_index != null;
    const rowIndex = hasExplicitRow
      ? Math.min(
          rowHeightsPx.length,
          Math.max(1, Math.trunc(Number(item.row_index ?? item.block_index)))
        )
      : itemRowIndex(item, rowHeightsPx);

    if (hasExplicitRow) {
      return clampCanvasItem({ ...item, row_index: rowIndex });
    }

    const { starts, bandHeights } = rowBandLayout(rowHeightsPx);
    const bandStart = starts[rowIndex - 1] ?? 0;
    const bandH = bandHeights[rowIndex - 1] || 100;
    return clampCanvasItem({
      ...item,
      row_index: rowIndex,
      y_pct: ((Number(item.y_pct) - bandStart) / bandH) * 100,
      h_pct: (Number(item.h_pct) / bandH) * 100,
    });
  });
}

/**
 * Convert between single-row and merged-row picture cell coordinates.
 * Stored geometry is always relative to the active picture cell (0–100).
 * Only call this when the merge span actually changes (e.g. merge toggle).
 */
export function toPictureCellLocal(item, startRowIndex, rowSpan, rowHeightsPx) {
  const heights = rowHeightsPx.slice(startRowIndex, startRowIndex + rowSpan);
  const totalPx = heights.reduce((sum, h) => sum + h, 0) || 1;
  const spanStored = Math.max(1, Number(item.cell_row_span) || 1);

  if (spanStored === rowSpan) {
    return clampCanvasItem({ ...item, cell_row_span: rowSpan });
  }

  if (rowSpan > 1 && spanStored === 1) {
    const rowPx = heights[0] || totalPx;
    const topPx = (Number(item.y_pct) / 100) * rowPx;
    const heightPx = (Number(item.h_pct) / 100) * rowPx;
    return clampCanvasItem({
      ...item,
      y_pct: (topPx / totalPx) * 100,
      h_pct: (heightPx / totalPx) * 100,
      cell_row_span: rowSpan,
    });
  }

  if (rowSpan === 1 && spanStored > 1) {
    const mergedHeights = rowHeightsPx.slice(
      startRowIndex,
      startRowIndex + spanStored
    );
    const mergedTotalPx = mergedHeights.reduce((sum, h) => sum + h, 0) || 1;
    const rowPx = heights[0] || mergedTotalPx;
    const topPx = (Number(item.y_pct) / 100) * mergedTotalPx;
    const heightPx = (Number(item.h_pct) / 100) * mergedTotalPx;
    return clampCanvasItem({
      ...item,
      y_pct: (topPx / rowPx) * 100,
      h_pct: (heightPx / rowPx) * 100,
      cell_row_span: 1,
    });
  }

  return clampCanvasItem({ ...item, cell_row_span: rowSpan });
}

/**
 * Items for one picture cell (single row or merged span), in local 0–100 coords.
 * Geometry is already cell-local; do not re-scale here (that would break positions
 * after save/load when cell_row_span is missing from storage).
 * @param {object[]} items
 * @param {number} startRowIndex 0-based row index where the cell starts
 * @param {number} rowSpan
 * @param {number[]} rowHeightsPx
 */
export function itemsForPictureCell(items, startRowIndex, rowSpan, rowHeightsPx) {
  const normalized = normalizeItemsToRows(items, rowHeightsPx);
  const ownerBlockIndex = startRowIndex + 1;
  const span = Math.max(1, Number(rowSpan) || 1);

  return normalized
    .filter((item) => item.row_index === ownerBlockIndex)
    .map((item) => clampCanvasItem({ ...item, cell_row_span: span }));
}

/**
 * Tag each canvas item with the active picture-cell row span from merge layout.
 * Use after load so merge/unmerge conversions use the correct prior span.
 */
export function stampPictureCellRowSpans(pageForm) {
  const textBlocks = normalizeTextBlocks(pageForm);
  const rowCount = textBlocks.length;
  const flags = pictureMergeNextFlags(pageForm, rowCount);
  const layout = computePictureRowSpans(rowCount, flags);
  const items = pageForm?.picture_canvas || [];

  return items.map((item) => {
    const rowIndex = Math.max(
      0,
      Math.min(rowCount - 1, (Number(item.row_index ?? item.block_index) || 1) - 1)
    );
    const span = layout.skip[rowIndex]
      ? 1
      : Math.max(1, Number(layout.spans[rowIndex]) || 1);
    return clampCanvasItem({ ...item, cell_row_span: span });
  });
}

export function replaceRowCanvasItems(allItems, blockIndex, nextRowItems, rowSpan = 1) {
  const nextList = nextRowItems || [];
  const replaceIds = new Set(
    nextList
      .map((item) => item?.id)
      .filter((id) => id != null && id !== "")
      .map((id) => String(id))
  );

  const others = (allItems || []).filter((item) => {
    const id = item?.id != null ? String(item.id) : "";
    if (id && replaceIds.has(id)) return false;
    const row = Number(item.row_index ?? item.block_index);
    return !(Number.isFinite(row) && row === blockIndex);
  });

  const tagged = nextList.map((item) =>
    clampCanvasItem({
      ...item,
      row_index: blockIndex,
      cell_row_span: Math.max(1, Number(rowSpan) || 1),
    })
  );
  return [...others, ...tagged];
}

/** Preview/print row band height (px) for one or more merged rows. */
export function previewPictureCellHeightPx(rowHeightsPx, startRowIndex, rowSpan = 1) {
  return rowHeightsPx
    .slice(startRowIndex, startRowIndex + rowSpan)
    .reduce((sum, height) => sum + height, 0);
}

/**
 * Picture cell height while editing: roomy for drag/resize, never smaller than print layout.
 */
export function editorPictureCellHeightPx(
  rowHeightsPx,
  startRowIndex,
  rowSpan = 1,
  formLocked = false
) {
  const previewHeight = previewPictureCellHeightPx(rowHeightsPx, startRowIndex, rowSpan);
  if (formLocked) return previewHeight;
  return Math.max(previewHeight, ROW_PICTURE_EDIT_MIN_HEIGHT_PX * rowSpan);
}
