import cv, { type ContourWrapper, type WrappedMat } from './opencvAdapter';
import { Skia, type SkPath } from '@shopify/react-native-skia';
import {
  BASEBOARD_SEMANTIC_NAME,
  classifyBgrPixelToSemantic,
  getCabinetQuantKeys,
  getSemanticColorByName,
  getWallQuantKeys,
  getBaseboardStripQuantKeys,
  isStrictBaseboardPixel,
  MASK_SEMANTIC_COLORS,
  type MaskSemanticColor,
} from './maskSemanticPalette';
import { getMaskRuntimeRevision, getMaskSegmentRuntimeConfig } from './maskSegmentRuntime';

function maskCfg() {
  return getMaskSegmentRuntimeConfig().mask;
}

export type RegionPickMap = {
  buffer: Uint8Array;
  cols: number;
  rows: number;
};

export type SegmentMaskResult = {
  regions: SegmentRegion[];
  pickMap: RegionPickMap;
  labels: Uint8Array;
  baseboardBinary: Uint8Array;
  segCols: number;
  segRows: number;
  /** splitWalls only: wall pixels → sub-region index 0..N-1, non-wall is WALL_SUB_LABEL_NONE */
  wallSubLabels?: Uint8Array;
};

export type SegmentRegion = {
  id: number;
  /** semantic partition name (door / cabinet / baseboard ...) */
  name: string;
  /** reference color hex */
  hex: string;
  /** reference color (BGR) */
  color: { b: number; g: number; r: number };
  polygons: { x: number; y: number }[][];
  /** paint/highlight mask: strict pixel strip, no black hole filling */
  maskPolygons?: { x: number; y: number }[][];
  hitPolygons?: { x: number; y: number }[][];
  outlinePolygons?: { x: number; y: number }[][];
  bbox: { x: number; y: number; w: number; h: number };
  area: number;
  /** baseboard etc. thin strip areas, click detection needs tolerance */
  thinStrip?: boolean;
};

const MORPH_KERNEL_SIZE = 5;
const MAX_DASH_OUTLINE_POLYGONS = 10;

type RowRun = { minX: number; maxX: number; y: number };

function bboxToPolygon(bbox: SegmentRegion['bbox']): NormPoint[] {
  return [
    { x: bbox.x, y: bbox.y },
    { x: bbox.x + bbox.w, y: bbox.y },
    { x: bbox.x + bbox.w, y: bbox.y + bbox.h },
    { x: bbox.x, y: bbox.y + bbox.h },
  ];
}

export function buildRegionOutlinePolygons(reg: SegmentRegion): NormPoint[][] {
  if (reg.outlinePolygons && reg.outlinePolygons.length > 0) {
    return reg.outlinePolygons;
  }
  if (reg.thinStrip || reg.polygons.length <= MAX_DASH_OUTLINE_POLYGONS) {
    return reg.polygons;
  }
  return [bboxToPolygon(reg.bbox)];
}

import { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion } from './maskOutlinePaths';

// Re-export for backward compatibility
export { buildAllRegionOutlinePaths, buildRegionOutlinePathForRegion };

function isBaseboardEntry(entry: PaletteEntry): boolean {
  return entry.name === BASEBOARD_SEMANTIC_NAME;
}

/** baseboard: only horizontal bridge within the same row, no vertical expansion */
function bridgeBaseboardHorizontally(
  binary: Uint8Array,
  cols: number,
  rows: number,
): Uint8Array {
  const out = new Uint8Array(binary);
  const halfW = maskCfg().kickBridgeHalfWPx;

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      if (!binary[y * cols + x]) {
        continue;
      }
      for (let dx = -halfW; dx <= halfW; dx++) {
        const nx = x + dx;
        if (nx < 0 || nx >= cols) {
          continue;
        }
        out[y * cols + nx] = 255;
      }
    }
  }

  return out;
}

function rowRunsToPolygons(
  runs: RowRun[],
  cols: number,
  rows: number,
): NormPoint[][] {
  return runs.map(run => [
    { x: run.minX / cols, y: run.y / rows },
    { x: (run.maxX + 1) / cols, y: run.y / rows },
    { x: (run.maxX + 1) / cols, y: (run.y + 1) / rows },
    { x: run.minX / cols, y: (run.y + 1) / rows },
  ]);
}

function quantChannelSlot(value: number): number {
  const q = Math.min(
    255,
    Math.round(value / maskCfg().quantStep) * maskCfg().quantStep,
  );
  if (q >= 192) {
    return q === 255 ? 4 : 3;
  }
  if (q >= 128) {
    return 2;
  }
  if (q >= 64) {
    return 1;
  }
  return 0;
}

function quantKeyIndex(b: number, g: number, r: number): number {
  return (
    quantChannelSlot(b) * 25 + quantChannelSlot(g) * 5 + quantChannelSlot(r)
  );
}

function getStripQuantIndices(): Set<number> {
  const revision = getMaskRuntimeRevision();
  if (stripIndicesRevision === revision && cachedStripQuantIndices) {
    return cachedStripQuantIndices;
  }
  cachedStripQuantIndices = new Set(
    [...getBaseboardStripQuantKeys()].map(key => {
      const [b, g, r] = key.split(',').map(part => Number(part));
      return quantKeyIndex(b, g, r);
    }),
  );
  stripIndicesRevision = revision;
  return cachedStripQuantIndices;
}

let stripIndicesRevision = -1;
let cachedStripQuantIndices: Set<number> | null = null;

let channelSlotLutRevision = -1;
let cachedChannelSlotLut: Uint8Array | null = null;

function buildQuantChannelSlotLut(quantStep: number): Uint8Array {
  const lut = new Uint8Array(256);
  for (let value = 0; value < 256; value++) {
    const q = Math.min(
      255,
      Math.round(value / quantStep) * quantStep,
    );
    if (q >= 192) {
      lut[value] = q === 255 ? 4 : 3;
    } else if (q >= 128) {
      lut[value] = 2;
    } else if (q >= 64) {
      lut[value] = 1;
    } else {
      lut[value] = 0;
    }
  }
  return lut;
}

function getQuantChannelSlotLut(): Uint8Array {
  const revision = getMaskRuntimeRevision();
  if (channelSlotLutRevision === revision && cachedChannelSlotLut) {
    return cachedChannelSlotLut;
  }
  cachedChannelSlotLut = buildQuantChannelSlotLut(maskCfg().quantStep);
  channelSlotLutRevision = revision;
  return cachedChannelSlotLut;
}

function quantSlotToChannel(slot: number): number {
  if (slot >= 4) {
    return 255;
  }
  if (slot >= 3) {
    return 192;
  }
  if (slot >= 2) {
    return 128;
  }
  if (slot >= 1) {
    return 64;
  }
  return 0;
}

function quantIndexToBgr(idx: number): [number, number, number] {
  const bSlot = (idx / 25) | 0;
  const gSlot = ((idx % 25) / 5) | 0;
  const rSlot = idx % 5;
  return [
    quantSlotToChannel(bSlot),
    quantSlotToChannel(gSlot),
    quantSlotToChannel(rSlot),
  ];
}

let semanticLutRevision = -1;
let cachedSemanticLut: Uint8Array | null = null;

let cachedStripQuantLut: Uint8Array | null = null;
let stripQuantLutRevision = -1;

function getStripQuantLut(): Uint8Array {
  const revision = getMaskRuntimeRevision();
  if (stripQuantLutRevision === revision && cachedStripQuantLut) {
    return cachedStripQuantLut;
  }
  const lut = new Uint8Array(125);
  for (const idx of getStripQuantIndices()) {
    lut[idx] = 1;
  }
  cachedStripQuantLut = lut;
  stripQuantLutRevision = revision;
  return lut;
}

function bboxFromBinary(
  binary: Uint8Array,
  cols: number,
  rows: number,
): SegmentRegion['bbox'] | null {
  let minX = cols;
  let minY = rows;
  let maxX = -1;
  let maxY = -1;

  for (let y = 0; y < rows; y++) {
    const row = y * cols;
    for (let x = 0; x < cols; x++) {
      if (!binary[row + x]) {
        continue;
      }
      if (x < minX) {
        minX = x;
      }
      if (x > maxX) {
        maxX = x;
      }
      if (y < minY) {
        minY = y;
      }
      if (y > maxY) {
        maxY = y;
      }
    }
  }

  if (maxX < 0) {
    return null;
  }

  return {
    x: minX / cols,
    y: minY / rows,
    w: (maxX - minX + 1) / cols,
    h: (maxY - minY + 1) / rows,
  };
}

/** build mask from binary mask row by row (for Skia PathBuilder) */
export function appendMaskBinaryToPathBuilder(
  binary: Uint8Array,
  cols: number,
  rows: number,
  rect: { x: number; y: number; w: number; h: number },
  builder: {
    moveTo: (x: number, y: number) => unknown;
    lineTo: (x: number, y: number) => unknown;
    close: () => unknown;
  },
  minRunPx = maskCfg().baseboardMinRunPx,
): void {
  for (let y = 0; y < rows; y++) {
    let runStart = -1;
    const normY0 = y / rows;
    const normY1 = (y + 1) / rows;
    const screenY0 = rect.y + normY0 * rect.h;
    const screenY1 = rect.y + normY1 * rect.h;

    for (let x = 0; x <= cols; x++) {
      const on = x < cols && binary[y * cols + x] > 0;
      if (on && runStart < 0) {
        runStart = x;
      }
      if (!on && runStart >= 0) {
        if (x - runStart >= minRunPx) {
          const normX0 = runStart / cols;
          const normX1 = x / cols;
          builder.moveTo(rect.x + normX0 * rect.w, screenY0);
          builder.lineTo(rect.x + normX1 * rect.w, screenY0);
          builder.lineTo(rect.x + normX1 * rect.w, screenY1);
          builder.lineTo(rect.x + normX0 * rect.w, screenY1);
          builder.close();
        }
        runStart = -1;
      }
    }
  }
}

/** build mask from semantic labels row by row (avoid maintaining multiple binary masks) */
export function appendLabelMaskToPathBuilder(
  labels: Uint8Array,
  semanticIndex: number,
  cols: number,
  rows: number,
  rect: { x: number; y: number; w: number; h: number },
  builder: {
    moveTo: (x: number, y: number) => unknown;
    lineTo: (x: number, y: number) => unknown;
    close: () => unknown;
  },
  minRunPx = maskCfg().baseboardMinRunPx,
): void {
  for (let y = 0; y < rows; y++) {
    let runStart = -1;
    const row = y * cols;
    const normY0 = y / rows;
    const normY1 = (y + 1) / rows;
    const screenY0 = rect.y + normY0 * rect.h;
    const screenY1 = rect.y + normY1 * rect.h;

    for (let x = 0; x <= cols; x++) {
      const on =
        x < cols &&
        labels[row + x] === semanticIndex;
      if (on && runStart < 0) {
        runStart = x;
      }
      if (!on && runStart >= 0) {
        if (x - runStart >= minRunPx) {
          const normX0 = runStart / cols;
          const normX1 = x / cols;
          builder.moveTo(rect.x + normX0 * rect.w, screenY0);
          builder.lineTo(rect.x + normX1 * rect.w, screenY0);
          builder.lineTo(rect.x + normX1 * rect.w, screenY1);
          builder.lineTo(rect.x + normX0 * rect.w, screenY1);
          builder.close();
        }
        runStart = -1;
      }
    }
  }
}

type PathBuilderLike = {
  moveTo: (x: number, y: number) => unknown;
  lineTo: (x: number, y: number) => unknown;
  close: () => unknown;
};

function appendRunRectToBuilder(
  runStart: number,
  runEnd: number,
  y: number,
  cols: number,
  rows: number,
  rect: { x: number; y: number; w: number; h: number },
  builder: PathBuilderLike,
  minRunPx: number,
): void {
  if (runEnd - runStart < minRunPx) {
    return;
  }
  const normY0 = y / rows;
  const normY1 = (y + 1) / rows;
  const screenY0 = rect.y + normY0 * rect.h;
  const screenY1 = rect.y + normY1 * rect.h;
  const normX0 = runStart / cols;
  const normX1 = runEnd / cols;
  builder.moveTo(rect.x + normX0 * rect.w, screenY0);
  builder.lineTo(rect.x + normX1 * rect.w, screenY0);
  builder.lineTo(rect.x + normX1 * rect.w, screenY1);
  builder.lineTo(rect.x + normX0 * rect.w, screenY1);
  builder.close();
}

export type RegionMaskData = {
  labels: Uint8Array;
  baseboardBinary: Uint8Array;
  cols: number;
  rows: number;
  wallSubLabels?: Uint8Array;
};

/** downsample mask path building (screen display does not need segmentation resolution, click still uses full resolution pickMap) */
export function downsampleMaskDataForPaths(
  maskData: RegionMaskData,
  maxLongSide: number,
): RegionMaskData {
  const { labels, baseboardBinary, cols, rows, wallSubLabels } = maskData;
  const longSide = Math.max(cols, rows);
  if (longSide <= maxLongSide) {
    return maskData;
  }

  const scale = maxLongSide / longSide;
  const dstCols = Math.max(1, Math.floor(cols * scale));
  const dstRows = Math.max(1, Math.floor(rows * scale));
  const outLabels = new Uint8Array(dstCols * dstRows);
  const outBaseboard = new Uint8Array(dstCols * dstRows);
  const outWallSub =
    wallSubLabels != null ? new Uint8Array(dstCols * dstRows) : undefined;
  if (outWallSub) {
    outWallSub.fill(255);
  }

  for (let y = 0; y < dstRows; y++) {
    const sy = Math.min(rows - 1, Math.floor((y * rows) / dstRows));
    const srcRow = sy * cols;
    const dstRow = y * dstCols;
    for (let x = 0; x < dstCols; x++) {
      const sx = Math.min(cols - 1, Math.floor((x * cols) / dstCols));
      const si = srcRow + sx;
      const di = dstRow + x;
      outLabels[di] = labels[si];
      outBaseboard[di] = baseboardBinary[si];
      if (outWallSub && wallSubLabels) {
        outWallSub[di] = wallSubLabels[si];
      }
    }
  }

  return {
    labels: outLabels,
    baseboardBinary: outBaseboard,
    cols: dstCols,
    rows: dstRows,
    wallSubLabels: outWallSub,
  };
}

/** single pass build all partition Skia mask paths (single label pass, avoid per pixel × semantic count loop) */
export function buildAllRegionMaskPaths(
  regions: SegmentRegion[],
  maskData: RegionMaskData,
  rect: { x: number; y: number; w: number; h: number },
): Map<number, SkPath> {
  const { labels, baseboardBinary, cols, rows } = maskData;
  const builders = new Map<number, PathBuilderLike>();
  const semanticColors = getMaskSegmentRuntimeConfig().mask.semanticColors;
  const regionIdBySemantic = new Int32Array(semanticColors.length);
  regionIdBySemantic.fill(-1);
  let baseboardRegionId: number | null = null;

  for (const reg of regions) {
    builders.set(reg.id, Skia.Path.Make() as unknown as PathBuilderLike);
    if (reg.thinStrip) {
      baseboardRegionId = reg.id;
      continue;
    }
    const semanticIndex = semanticColors.findIndex(
      entry => entry.name === reg.name,
    );
    if (semanticIndex >= 0) {
      regionIdBySemantic[semanticIndex] = reg.id;
    }
  }

  const semanticCount = semanticColors.length;
  const minRunPx = maskCfg().baseboardMinRunPx;

  for (let y = 0; y < rows; y++) {
    let baseboardRunStart = -1;
    let labelRunStart = -1;
    let labelRunSemantic = -1;
    const row = y * cols;

    for (let x = 0; x <= cols; x++) {
      if (baseboardRegionId != null) {
        const bbOn = x < cols && baseboardBinary[row + x] > 0;
        if (bbOn && baseboardRunStart < 0) {
          baseboardRunStart = x;
        }
        if (!bbOn && baseboardRunStart >= 0) {
          appendRunRectToBuilder(
            baseboardRunStart,
            x,
            y,
            cols,
            rows,
            rect,
            builders.get(baseboardRegionId)!,
            minRunPx,
          );
          baseboardRunStart = -1;
        }
      }

      let activeSemantic = -1;
      if (x < cols) {
        const si = labels[row + x];
        if (si < semanticCount && regionIdBySemantic[si] >= 0) {
          activeSemantic = si;
        }
      }

      if (activeSemantic !== labelRunSemantic) {
        if (labelRunSemantic >= 0 && labelRunStart >= 0) {
          const regionId = regionIdBySemantic[labelRunSemantic];
          appendRunRectToBuilder(
            labelRunStart,
            x,
            y,
            cols,
            rows,
            rect,
            builders.get(regionId)!,
            minRunPx,
          );
        }
        labelRunSemantic = activeSemantic;
        labelRunStart = activeSemantic >= 0 ? x : -1;
      }
    }
  }

  const paths = new Map<number, SkPath>();
  for (const [regionId, builder] of builders) {
    paths.set(regionId, builder as unknown as SkPath);
  }
  return paths;
}

function collectRowRuns(
  binary: Uint8Array,
  cols: number,
  rows: number,
  minRunPx: number,
): RowRun[] {
  const runs: RowRun[] = [];

  for (let y = 0; y < rows; y++) {
    let runStart = -1;
    for (let x = 0; x <= cols; x++) {
      const on = x < cols && binary[y * cols + x] > 0;
      if (on && runStart < 0) {
        runStart = x;
      }
      if (!on && runStart >= 0) {
        if (x - runStart >= minRunPx) {
          runs.push({ minX: runStart, maxX: x - 1, y });
        }
        runStart = -1;
      }
    }
  }

  return runs;
}

function bboxFromPolygons(polygons: NormPoint[][]): SegmentRegion['bbox'] | null {
  if (polygons.length === 0) {
    return null;
  }

  let minX = 1;
  let minY = 1;
  let maxX = 0;
  let maxY = 0;

  for (const polygon of polygons) {
    for (const point of polygon) {
      minX = Math.min(minX, point.x);
      minY = Math.min(minY, point.y);
      maxX = Math.max(maxX, point.x);
      maxY = Math.max(maxY, point.y);
    }
  }

  return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
}

/** baseboard: row-wise 1px strips fitted to mask; horizontally bridged strips used for tap lookup */
function extractBaseboardRowPolygons(
  binary: Uint8Array,
  cols: number,
  rows: number,
): {
  polygons: NormPoint[][];
  hitPolygons: NormPoint[][];
  totalArea: number;
  bbox: SegmentRegion['bbox'] | null;
} {
  let totalArea = 0;
  for (let i = 0; i < binary.length; i++) {
    if (binary[i]) {
      totalArea += 1;
    }
  }

  const runs = collectRowRuns(binary, cols, rows, maskCfg().baseboardMinRunPx);
  const polygons = rowRunsToPolygons(runs, cols, rows);

  const bridged = bridgeBaseboardHorizontally(binary, cols, rows);
  const bridgedRuns = collectRowRuns(
    bridged,
    cols,
    rows,
    maskCfg().baseboardMinRunPx,
  );
  const hitPolygons = rowRunsToPolygons(bridgedRuns, cols, rows);
  const bbox = bboxFromPolygons(polygons);


  return {
    polygons,
    hitPolygons,
    totalArea,
    bbox,
  };
}

function cloneBinary(binary: Uint8Array): Uint8Array {
  return new Uint8Array(binary);
}

function subtractBinary(target: Uint8Array, mask: Uint8Array): void {
  for (let i = 0; i < target.length; i++) {
    if (mask[i]) {
      target[i] = 0;
    }
  }
}

type BgrColor = { b: number; g: number; r: number };
type NormPoint = { x: number; y: number };
type PaletteEntry = {
  label: number;
  name: string;
  hex: string;
  color: BgrColor;
};

function minPalettePixels(cols: number, rows: number): number {
  return Math.max(300, Math.floor((cols * rows) / 2000));
}

function minPixelsForSemantic(
  name: string,
  cols: number,
  rows: number,
): number {
  const base = minPalettePixels(cols, rows);
  if (!maskCfg().secondarySemanticNames.has(name)) {
    return base;
  }
  return Math.max(
    base,
    Math.floor(cols * rows * maskCfg().secondaryMinPixelRatio),
  );
}

function quantizeChannel(value: number): number {
  return Math.min(
    255,
    Math.round(value / maskCfg().quantStep) * maskCfg().quantStep,
  );
}

function maskQuantKey(b: number, g: number, r: number): string {
  return `${quantizeChannel(b)},${quantizeChannel(g)},${quantizeChannel(r)}`;
}

function dilateBinaryBox(
  source: Uint8Array,
  cols: number,
  rows: number,
  radiusX: number,
  radiusY: number,
): Uint8Array {
  const temp = new Uint8Array(source.length);
  const out = new Uint8Array(source.length);

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      if (!source[y * cols + x]) {
        continue;
      }
      const minX = Math.max(0, x - radiusX);
      const maxX = Math.min(cols - 1, x + radiusX);
      for (let nx = minX; nx <= maxX; nx++) {
        temp[y * cols + nx] = 255;
      }
    }
  }

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      if (!temp[y * cols + x]) {
        continue;
      }
      const minY = Math.max(0, y - radiusY);
      const maxY = Math.min(rows - 1, y + radiusY);
      for (let ny = minY; ny <= maxY; ny++) {
        out[ny * cols + x] = 255;
      }
    }
  }

  return out;
}

function buildWallCabinetJunctionMask(
  buffer: Uint8Array,
  cols: number,
  rows: number,
): Uint8Array {
  const wall = new Uint8Array(cols * rows);
  const cabinet = new Uint8Array(cols * rows);

  for (let i = 0; i < cols * rows; i++) {
    const o = i * 3;
    const b = buffer[o];
    const g = buffer[o + 1];
    const r = buffer[o + 2];
    if (isIgnoredColor(b, g, r)) {
      continue;
    }
    const key = maskQuantKey(b, g, r);
    if (getWallQuantKeys().has(key)) {
      wall[i] = 255;
    }
    if (getCabinetQuantKeys().has(key)) {
      cabinet[i] = 255;
    }
  }

  const wallNear = dilateBinaryBox(
    wall,
    cols,
    rows,
    maskCfg().junctionHRadiusPx,
    maskCfg().junctionVRadiusPx,
  );
  const cabinetNear = dilateBinaryBox(
    cabinet,
    cols,
    rows,
    maskCfg().junctionHRadiusPx,
    maskCfg().junctionVRadiusPx,
  );
  const junction = new Uint8Array(cols * rows);
  for (let i = 0; i < junction.length; i++) {
    if (wallNear[i] && cabinetNear[i]) {
      junction[i] = 255;
    }
  }
  return junction;
}

function computeStrictBaseboardBand(
  strictBaseboard: Uint8Array,
  cols: number,
  rows: number,
): { minY: number; maxY: number } | null {
  let minY = rows;
  let maxY = -1;

  for (let y = 0; y < rows; y++) {
    const row = y * cols;
    for (let x = 0; x < cols; x++) {
      if (!strictBaseboard[row + x]) {
        continue;
      }
      if (y < minY) {
        minY = y;
      }
      if (y > maxY) {
        maxY = y;
      }
    }
  }

  if (maxY < 0) {
    return null;
  }
  return { minY, maxY };
}

/** Keep only junction strips close to real baseboard, avoiding false positives from wall-cabinet boundary fragments */
function isJunctionNearStrictBaseboard(
  idx: number,
  strictBaseboard: Uint8Array,
  cols: number,
  rows: number,
  band: { minY: number; maxY: number } | null,
): boolean {
  if (!band) {
    return false;
  }

  const x = idx % cols;
  const y = (idx - x) / cols;
  if (
    y < band.minY - maskCfg().baseboardJunctionRowMarginPx ||
    y > band.maxY + maskCfg().baseboardJunctionRowMarginPx
  ) {
    return false;
  }

  const halfW = maskCfg().kickBridgeHalfWPx;
  for (
    let dy = -maskCfg().baseboardJunctionVReachPx;
    dy <= maskCfg().baseboardJunctionVReachPx;
    dy++
  ) {
    const ny = y + dy;
    if (ny < 0 || ny >= rows) {
      continue;
    }
    const row = ny * cols;
    for (let dx = -halfW; dx <= halfW; dx++) {
      const nx = x + dx;
      if (nx < 0 || nx >= cols) {
        continue;
      }
      if (strictBaseboard[row + nx]) {
        return true;
      }
    }
  }

  return false;
}

function buildBaseboardBinary(
  buffer: Uint8Array,
  cols: number,
  rows: number,
  junctionMask?: Uint8Array,
): Uint8Array {
  const binary = new Uint8Array(cols * rows);
  const junction =
    junctionMask ?? buildWallCabinetJunctionMask(buffer, cols, rows);

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      const idx = y * cols + x;
      const o = idx * 3;
      const b = buffer[o];
      const g = buffer[o + 1];
      const r = buffer[o + 2];
      if (isIgnoredColor(b, g, r)) {
        continue;
      }

      if (isStrictBaseboardPixel(b, g, r)) {
        binary[idx] = 255;
      }
    }
  }

  const band = computeStrictBaseboardBand(binary, cols, rows);
  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      const idx = y * cols + x;
      if (binary[idx]) {
        continue;
      }
      const o = idx * 3;
      const b = buffer[o];
      const g = buffer[o + 1];
      const r = buffer[o + 2];
      if (isIgnoredColor(b, g, r)) {
        continue;
      }

      const key = maskQuantKey(b, g, r);
      if (
        getBaseboardStripQuantKeys().has(key) &&
        junction[idx] &&
        isJunctionNearStrictBaseboard(idx, binary, cols, rows, band)
      ) {
        binary[idx] = 255;
      }
    }
  }

  return binary;
}

export function buildBaseboardBinaryFromMask(
  buffer: Uint8Array,
  cols: number,
  rows: number,
): Uint8Array {
  return buildBaseboardBinary(buffer, cols, rows);
}

/** Upscale segmentation-resolution baseboard binary to tap-lookup resolution via nearest-neighbor (avoids full-image junction recomputation) */
export function upscaleBinaryMask(
  src: Uint8Array,
  srcCols: number,
  srcRows: number,
  dstCols: number,
  dstRows: number,
): Uint8Array {
  const dst = new Uint8Array(dstCols * dstRows);
  for (let y = 0; y < dstRows; y++) {
    const sy = Math.min(srcRows - 1, Math.floor((y * srcRows) / dstRows));
    const srcRow = sy * srcCols;
    const dstRow = y * dstCols;
    for (let x = 0; x < dstCols; x++) {
      const sx = Math.min(srcCols - 1, Math.floor((x * srcCols) / dstCols));
      dst[dstRow + x] = src[srcRow + sx];
    }
  }
  return dst;
}

function buildMaskPolygonsFromBinary(
  binary: Uint8Array,
  cols: number,
  rows: number,
): NormPoint[][] {
  return rowRunsToPolygons(
    collectRowRuns(binary, cols, rows, maskCfg().baseboardMinRunPx),
    cols,
    rows,
  );
}

export function isBaseboardMaskPixel(
  buffer: Uint8Array,
  cols: number,
  rows: number,
  x: number,
  y: number,
  baseboardBinary?: Uint8Array | null,
): boolean {
  if (x < 0 || y < 0 || x >= cols || y >= rows) {
    return false;
  }
  if (baseboardBinary) {
    return baseboardBinary[y * cols + x] > 0;
  }
  const o = (y * cols + x) * 3;
  const b = buffer[o];
  const g = buffer[o + 1];
  const r = buffer[o + 2];
  if (isIgnoredColor(b, g, r)) {
    return false;
  }
  if (isStrictBaseboardPixel(b, g, r)) {
    return true;
  }
  const key = maskQuantKey(b, g, r);
  if (!getBaseboardStripQuantKeys().has(key)) {
    return false;
  }
  const junction = buildWallCabinetJunctionMask(buffer, cols, rows);
  return junction[y * cols + x] > 0;
}

export { isStrictBaseboardPixel as isBaseboardPixel } from './maskSemanticPalette';

export function getMaskQuantKey(b: number, g: number, r: number): string {
  return maskQuantKey(b, g, r);
}

/** @deprecated Use isBaseboardMaskPixel */
export function isKickPlatePixel(b: number, g: number, r: number): boolean {
  return isStrictBaseboardPixel(b, g, r);
}


function mergeBBox(
  bbox: SegmentRegion['bbox'],
  next: SegmentRegion['bbox'],
): SegmentRegion['bbox'] {
  const x1 = Math.min(bbox.x, next.x);
  const y1 = Math.min(bbox.y, next.y);
  const x2 = Math.max(bbox.x + bbox.w, next.x + next.w);
  const y2 = Math.max(bbox.y + bbox.h, next.y + next.h);
  return { x: x1, y: y1, w: x2 - x1, h: y2 - y1 };
}

function isIgnoredColor(b: number, g: number, r: number): boolean {
  const threshold = maskCfg().blackThreshold;
  return b < threshold && g < threshold && r < threshold;
}

function countBinaryPixels(binary: Uint8Array): number {
  let count = 0;
  for (let i = 0; i < binary.length; i++) {
    if (binary[i]) {
      count += 1;
    }
  }
  return count;
}

const IGNORE_SEMANTIC_INDEX = 255;

let nameToIndexRevision = -1;
let cachedNameToIndex: Map<string, number> | null = null;

function getSemanticNameToIndex(): Map<string, number> {
  const revision = getMaskRuntimeRevision();
  if (nameToIndexRevision === revision && cachedNameToIndex) {
    return cachedNameToIndex;
  }
  const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
  cachedNameToIndex = new Map(
    colors.map((entry, index) => [entry.name, index]),
  );
  nameToIndexRevision = revision;
  return cachedNameToIndex;
}

function createSemanticLut(): Uint8Array {
  const revision = getMaskRuntimeRevision();
  if (semanticLutRevision === revision && cachedSemanticLut) {
    return cachedSemanticLut;
  }

  const lut = new Uint8Array(125);
  lut.fill(IGNORE_SEMANTIC_INDEX);
  const colors = getMaskSegmentRuntimeConfig().mask.semanticColors;
  const nameToIndex = getSemanticNameToIndex();
  for (const entry of colors) {
    const semanticIndex = nameToIndex.get(entry.name);
    if (semanticIndex === undefined) {
      continue;
    }
    const { b, g, r } = entry.bgr;
    lut[quantKeyIndex(b, g, r)] = semanticIndex;
  }

  for (let idx = 0; idx < 125; idx++) {
    if (lut[idx] !== IGNORE_SEMANTIC_INDEX) {
      continue;
    }
    const [b, g, r] = quantIndexToBgr(idx);
    const name = classifyBgrPixelToSemantic(b, g, r);
    lut[idx] = nameToIndex.get(name) ?? IGNORE_SEMANTIC_INDEX;
  }

  cachedSemanticLut = lut;
  semanticLutRevision = revision;
  return lut;
}


type SemanticLayout = {
  labels: Uint8Array;
  counts: Map<string, number>;
  bboxes: Map<string, SegmentRegion['bbox']>;
  stripIndices: number[];
  strictBaseboard: Uint8Array;
  strictBaseboardCount: number;
};

/** Single-pass scan: pixel semantic labels + pixel count + bbox (avoids writing multiple binary images) */
function buildSemanticLayout(
  buffer: Uint8Array,
  cols: number,
  rows: number,
): SemanticLayout {
  const pixelCount = cols * rows;
  const labels = new Uint8Array(pixelCount);
  labels.fill(IGNORE_SEMANTIC_INDEX);
  const counts = new Map<string, number>();
  const bboxes = new Map<string, SegmentRegion['bbox']>();
  const semanticLut = createSemanticLut();
  const nameToIndex = getSemanticNameToIndex();
  const indexToName = getMaskSegmentRuntimeConfig().mask.semanticColors.map(
    entry => entry.name,
  );
  const semanticCount = indexToName.length;
  const blackThreshold = maskCfg().blackThreshold;
  const channelSlotLut = getQuantChannelSlotLut();
  const stripQuantLut = getStripQuantLut();
  const minX = new Int32Array(semanticCount);
  const minY = new Int32Array(semanticCount);
  const maxX = new Int32Array(semanticCount);
  const maxY = new Int32Array(semanticCount);
  const hitMask = new Uint8Array(semanticCount);
  const countArr = new Int32Array(semanticCount);
  const stripIndices: number[] = [];
  const strictBaseboard = new Uint8Array(pixelCount);
  let strictBaseboardCount = 0;
  const baseboardIdx = nameToIndex.get(BASEBOARD_SEMANTIC_NAME);
  minX.fill(cols);
  minY.fill(rows);
  maxX.fill(-1);
  maxY.fill(-1);

  const buf = buffer;
  for (let y = 0; y < rows; y++) {
    const row = y * cols;
    for (let x = 0; x < cols; x++) {
      const i = row + x;
      const o = i * 3;
      const b = buf[o];
      const g = buf[o + 1];
      const r = buf[o + 2];
      if (b < blackThreshold && g < blackThreshold && r < blackThreshold) {
        continue;
      }

      const lutIdx =
        channelSlotLut[b] * 25 + channelSlotLut[g] * 5 + channelSlotLut[r];
      if (stripQuantLut[lutIdx]) {
        stripIndices.push(i);
      }

      const semanticIndex = semanticLut[lutIdx];
      if (semanticIndex === IGNORE_SEMANTIC_INDEX) {
        continue;
      }

      labels[i] = semanticIndex;
      hitMask[semanticIndex] = 1;
      countArr[semanticIndex] += 1;

      if (semanticIndex === baseboardIdx) {
        strictBaseboard[i] = 255;
        strictBaseboardCount += 1;
      }

      if (x < minX[semanticIndex]) {
        minX[semanticIndex] = x;
      }
      if (x > maxX[semanticIndex]) {
        maxX[semanticIndex] = x;
      }
      if (y < minY[semanticIndex]) {
        minY[semanticIndex] = y;
      }
      if (y > maxY[semanticIndex]) {
        maxY[semanticIndex] = y;
      }
    }
  }

  const invCols = 1 / cols;
  const invRows = 1 / rows;
  for (let semanticIndex = 0; semanticIndex < semanticCount; semanticIndex++) {
    if (!hitMask[semanticIndex]) {
      continue;
    }
    const name = indexToName[semanticIndex];
    counts.set(name, countArr[semanticIndex]);
    bboxes.set(name, {
      x: minX[semanticIndex] * invCols,
      y: minY[semanticIndex] * invRows,
      w: (maxX[semanticIndex] - minX[semanticIndex] + 1) * invCols,
      h: (maxY[semanticIndex] - minY[semanticIndex] + 1) * invRows,
    });
  }

  return {
    labels,
    counts,
    bboxes,
    stripIndices,
    strictBaseboard,
    strictBaseboardCount,
  };
}

function buildJunctionAtStripPixels(
  labels: Uint8Array,
  stripIndices: number[],
  cols: number,
  rows: number,
): { junction: Uint8Array; junctionIndices: number[] } {
  const junction = new Uint8Array(cols * rows);
  const junctionIndices: number[] = [];
  const nameToIndex = getSemanticNameToIndex();
  const wallIdx = nameToIndex.get('wall');
  const cabinetIdx = nameToIndex.get('cabinet');
  const junctionH = maskCfg().junctionHRadiusPx;
  const junctionV = maskCfg().junctionVRadiusPx;
  if (
    wallIdx === undefined ||
    cabinetIdx === undefined ||
    stripIndices.length === 0
  ) {
    return { junction, junctionIndices };
  }

  for (const idx of stripIndices) {
    const x = idx % cols;
    const y = (idx - x) / cols;

    let hasWall = false;
    let hasCabinet = false;
    const minY = Math.max(0, y - junctionV);
    const maxY = Math.min(rows - 1, y + junctionV);
    const minX = Math.max(0, x - junctionH);
    const maxX = Math.min(cols - 1, x + junctionH);

    for (let ny = minY; ny <= maxY && !(hasWall && hasCabinet); ny++) {
      const rowBase = ny * cols;
      for (let nx = minX; nx <= maxX && !(hasWall && hasCabinet); nx++) {
        const label = labels[rowBase + nx];
        if (label === wallIdx) {
          hasWall = true;
        } else if (label === cabinetIdx) {
          hasCabinet = true;
        }
      }
    }

    if (hasWall && hasCabinet) {
      junction[idx] = 255;
      junctionIndices.push(idx);
    }
  }

  return { junction, junctionIndices };
}

function finalizeBaseboardBinary(
  strictBaseboard: Uint8Array,
  strictCount: number,
  junctionIndices: number[],
  cols: number,
  rows: number,
): { binary: Uint8Array; pixelCount: number } {
  const binary = new Uint8Array(strictBaseboard);
  const band = computeStrictBaseboardBand(binary, cols, rows);
  let pixelCount = strictCount;
  for (const idx of junctionIndices) {
    if (
      !isJunctionNearStrictBaseboard(idx, binary, cols, rows, band)
    ) {
      continue;
    }
    if (!binary[idx]) {
      pixelCount += 1;
    }
    binary[idx] = 255;
  }
  return { binary, pixelCount };
}

function buildPickMapAndWorkAreas(
  labels: Uint8Array,
  indexToName: string[],
  nameToId: Map<string, number>,
  baseboardBinary: Uint8Array,
  cols: number,
  rows: number,
): { pick: Uint8Array; workAreas: Map<string, number> } {
  const pixelCount = cols * rows;
  const pick = new Uint8Array(pixelCount);
  const workAreas = new Map<string, number>();
  const baseboardName = BASEBOARD_SEMANTIC_NAME;
  const baseboardId = nameToId.get(baseboardName);
  const baseboardCode =
    baseboardId === undefined ? 0 : baseboardId + 1;

  for (let i = 0; i < pixelCount; i++) {
    if (baseboardBinary[i]) {
      if (baseboardCode > 0) {
        pick[i] = baseboardCode;
      }
      workAreas.set(
        baseboardName,
        (workAreas.get(baseboardName) ?? 0) + 1,
      );
      continue;
    }

    const semanticIndex = labels[i];
    if (semanticIndex === IGNORE_SEMANTIC_INDEX) {
      continue;
    }

    const name = indexToName[semanticIndex];
    if (!name) {
      continue;
    }

    const regionId = nameToId.get(name);
    if (regionId !== undefined) {
      pick[i] = regionId + 1;
    }
    workAreas.set(name, (workAreas.get(name) ?? 0) + 1);
  }

  return { pick, workAreas };
}

/**
 * 1-pass 8-neighbour majority-vote dilate on the pick buffer.
 * For each zero pixel, count the 8-connected neighbours by their non-zero
 * pick code.  If any single code appears in ≥ 4 neighbours, fill the pixel
 * with that code (majority rule).
 *
 * Compared to the old 4-neighbour "all-must-agree" rule this handles:
 *   - diagonal holes inside a region (8-connectivity)
 *   - narrow door / furniture strips where a hole borders both the strip
 *     AND a neighbouring wall region — majority vote picks the region that
 *     occupies more of the 8-pixel perimeter
 *
 * Still reads from the ORIGINAL pick buffer to prevent cascade overflow.
 * Cost: O(N) with ~20 ops/pixel — negligible relative to segmentation.
 */
function dilatePickBuffer1px(
  pick: Uint8Array,
  cols: number,
  rows: number,
): Uint8Array {
  const pixelCount = cols * rows;
  const dst = new Uint8Array(pixelCount);
  dst.set(pick);

  for (let y = 1; y < rows - 1; y++) {
    for (let x = 1; x < cols - 1; x++) {
      const i = y * cols + x;
      if (pick[i] !== 0) continue;

      // Read 8 neighbours from the ORIGINAL pick buffer to avoid cascade.
      const n = [
        pick[(y - 1) * cols + (x - 1)], // top-left
        pick[(y - 1) * cols + x],       // top
        pick[(y - 1) * cols + (x + 1)], // top-right
        pick[y * cols + (x - 1)],       // left
        pick[y * cols + (x + 1)],       // right
        pick[(y + 1) * cols + (x - 1)], // bottom-left
        pick[(y + 1) * cols + x],       // bottom
        pick[(y + 1) * cols + (x + 1)], // bottom-right
      ];

      // Count occurrences of each non-zero code.
      const counts: Record<number, number> = {};
      for (let k = 0; k < 8; k++) {
        const code = n[k];
        if (code !== 0) {
          counts[code] = (counts[code] ?? 0) + 1;
        }
      }

      // Majority rule: ≥ 4 of 8 neighbours share the same code.
      for (const codeStr of Object.keys(counts)) {
        const code = Number(codeStr);
        if (counts[code] >= 4) {
          dst[i] = code;
          break;
        }
      }
    }
  }
  return dst;
}

function paletteFromCounts(
  counts: Map<string, number>,
  cols: number,
  rows: number,
): PaletteEntry[] {
  const orderedSemantics = getMaskSegmentRuntimeConfig().mask.semanticColors.map(
    entry => entry.name,
  );

  return orderedSemantics
    .map(name => {
      const pixelCount = counts.get(name) ?? 0;
      if (pixelCount < minPixelsForSemantic(name, cols, rows)) {
        return null;
      }
      const ref = getSemanticColorByName(name) as MaskSemanticColor;
      return {
        label: orderedSemantics.indexOf(name),
        name,
        hex: ref.hex,
        color: { ...ref.bgr },
      } satisfies PaletteEntry;
    })
    .filter((entry): entry is PaletteEntry => entry != null)
    .sort((a, b) => (counts.get(b.name) ?? 0) - (counts.get(a.name) ?? 0))
    .slice(0, maskCfg().maxRegionColors);
}

async function contourToPolygon(
  contour: ContourWrapper,
  cols: number,
  rows: number,
  minArea: number,
  approxEpsilon: number,
): Promise<{
  polygon: NormPoint[];
  bbox: SegmentRegion['bbox'];
  area: number;
} | null> {
  const area = await cv.contourArea(contour);
  if (area < minArea) {
    return null;
  }

  const rect = await cv.boundingRect(contour);
  const perimeter = await cv.arcLength(contour, true);
  const maxEpsilonPx = Math.max(cols, rows) * 0.01;
  const thinSide = Math.min(rect.width, rect.height);
  const epsilonPx = Math.max(
    1.5,
    Math.min(
      perimeter * approxEpsilon,
      maxEpsilonPx,
      Math.max(2, thinSide * 0.12),
    ),
  );

  const points = await cv.approxPolyDP(contour, epsilonPx, true);
  if (points.length < 3) {
    return null;
  }

  let minX = cols;
  let minY = rows;
  let maxX = 0;
  let maxY = 0;
  const polygon = points.map(point => {
    minX = Math.min(minX, point.x);
    maxX = Math.max(maxX, point.x);
    minY = Math.min(minY, point.y);
    maxY = Math.max(maxY, point.y);
    return { x: point.x / cols, y: point.y / rows };
  });

  return {
    polygon,
    area,
    bbox: {
      x: minX / cols,
      y: minY / rows,
      w: (maxX - minX + 1) / cols,
      h: (maxY - minY + 1) / rows,
    },
  };
}

function extractPolygonsFromBinaryJs(
  binary: Uint8Array,
  cols: number,
  rows: number,
  minArea: number,
): {
  polygons: NormPoint[][];
  totalArea: number;
  bbox: SegmentRegion['bbox'] | null;
} {
  const visited = new Uint8Array(cols * rows);
  const polygons: NormPoint[][] = [];
  let totalArea = 0;
  let bbox: SegmentRegion['bbox'] | null = null;

  for (let y = 0; y < rows; y++) {
    for (let x = 0; x < cols; x++) {
      const idx = y * cols + x;
      if (!binary[idx] || visited[idx]) {
        continue;
      }

      let minX = x;
      let maxX = x;
      let minY = y;
      let maxY = y;
      let area = 0;
      const stack: Array<[number, number]> = [[x, y]];
      visited[idx] = 1;

      while (stack.length > 0) {
        const [cx, cy] = stack.pop()!;
        area += 1;
        minX = Math.min(minX, cx);
        maxX = Math.max(maxX, cx);
        minY = Math.min(minY, cy);
        maxY = Math.max(maxY, cy);

        if (cx > 0) {
          const left = cy * cols + (cx - 1);
          if (binary[left] && !visited[left]) {
            visited[left] = 1;
            stack.push([cx - 1, cy]);
          }
        }
        if (cx + 1 < cols) {
          const right = cy * cols + (cx + 1);
          if (binary[right] && !visited[right]) {
            visited[right] = 1;
            stack.push([cx + 1, cy]);
          }
        }
        if (cy > 0) {
          const up = (cy - 1) * cols + cx;
          if (binary[up] && !visited[up]) {
            visited[up] = 1;
            stack.push([cx, cy - 1]);
          }
        }
        if (cy + 1 < rows) {
          const down = (cy + 1) * cols + cx;
          if (binary[down] && !visited[down]) {
            visited[down] = 1;
            stack.push([cx, cy + 1]);
          }
        }
      }

      if (area < minArea) {
        continue;
      }

      const polygon: NormPoint[] = [
        { x: minX / cols, y: minY / rows },
        { x: (maxX + 1) / cols, y: minY / rows },
        { x: (maxX + 1) / cols, y: (maxY + 1) / rows },
        { x: minX / cols, y: (maxY + 1) / rows },
      ];
      const partBbox = {
        x: minX / cols,
        y: minY / rows,
        w: (maxX - minX + 1) / cols,
        h: (maxY - minY + 1) / rows,
      };

      polygons.push(polygon);
      totalArea += area;
      bbox = bbox ? mergeBBox(bbox, partBbox) : partBbox;
    }
  }

  return { polygons, totalArea, bbox };
}

async function extractPolygonsFromBinary(
  binary: Uint8Array,
  cols: number,
  rows: number,
  minArea: number,
  approxEpsilon: number,
): Promise<{
  polygons: NormPoint[][];
  totalArea: number;
  bbox: SegmentRegion['bbox'] | null;
}> {
  const binaryMat = cv.binaryBufferToMat(binary, cols, rows);
  const closed = cv.createMat(cols, rows, 1);

  try {
    const kernel = await cv.getStructuringElement(cv.MORPH_ELLIPSE, {
      width: MORPH_KERNEL_SIZE,
      height: MORPH_KERNEL_SIZE,
    });

    try {
      await cv.morphologyEx(binaryMat, closed, cv.MORPH_OPEN, kernel);
    } finally {
      kernel.release();
    }

    const contours = await cv.findContours(
      closed,
      cv.RETR_EXTERNAL,
      cv.CHAIN_APPROX_SIMPLE,
    );

    const polygons: NormPoint[][] = [];
    let totalArea = 0;
    let bbox: SegmentRegion['bbox'] | null = null;

    for (const contour of contours) {
      try {
        const part = await contourToPolygon(
          contour,
          cols,
          rows,
          minArea,
          approxEpsilon,
        );
        if (!part) {
          continue;
        }

        polygons.push(part.polygon);
        totalArea += part.area;
        bbox = bbox ? mergeBBox(bbox, part.bbox) : part.bbox;
      } finally {
        contour.release();
      }
    }

    return { polygons, totalArea, bbox };
  } finally {
    binaryMat.release();
    closed.release();
  }
}

export async function extractRegionsFromMaskBuffer(
  buffer: Uint8Array,
  cols: number,
  rows: number,
  _options: {
    minArea: number;
    approxEpsilon: number;
  },
): Promise<SegmentMaskResult> {
  return extractRegionsFromMaskBufferSync(buffer, cols, rows, _options);
}

export function extractRegionsFromMaskBufferSync(
  buffer: Uint8Array,
  cols: number,
  rows: number,
  _options: {
    minArea: number;
    approxEpsilon: number;
  },
): SegmentMaskResult {
  const layout = buildSemanticLayout(buffer, cols, rows);

  const baseboardStart = __DEV__ ? performance.now() : 0;
  const { junctionIndices } = buildJunctionAtStripPixels(
    layout.labels,
    layout.stripIndices,
    cols,
    rows,
  );
  const { binary: baseboardBinary, pixelCount: baseboardPixels } =
    finalizeBaseboardBinary(
      layout.strictBaseboard,
      layout.strictBaseboardCount,
      junctionIndices,
      cols,
      rows,
    );
  layout.counts.set(BASEBOARD_SEMANTIC_NAME, baseboardPixels);
  
  const paletteEntries = paletteFromCounts(layout.counts, cols, rows);

  if (paletteEntries.length === 0) {
    return {
      regions: [],
      pickMap: { buffer: new Uint8Array(cols * rows), cols, rows },
      labels: layout.labels,
      baseboardBinary,
      segCols: cols,
      segRows: rows,
    };
  }

  const indexToName = getMaskSegmentRuntimeConfig().mask.semanticColors.map(
    entry => entry.name,
  );
  const regionResults = paletteEntries.map(
    (entry): SegmentRegion | null => {
      const { label, name, hex, color } = entry;
      const isBaseboard = isBaseboardEntry(entry);
      try {
        const finalBbox = isBaseboard
          ? bboxFromBinary(baseboardBinary, cols, rows)
          : layout.bboxes.get(name);

        if (!finalBbox) {
          if (__DEV__) {
            console.warn(`[MaskSegment] ${name} no valid contour, skipped`);
          }
          return null;
        }

        const finalPolygons = [bboxToPolygon(finalBbox)];

        return {
          id: label,
          name,
          hex,
          color,
          area: layout.counts.get(name) ?? 0,
          bbox: finalBbox,
          polygons: finalPolygons,
          outlinePolygons: finalPolygons,
          thinStrip: isBaseboard,
        } as SegmentRegion;
      } catch (error) {
        if (__DEV__) {
          console.warn(
            `[MaskSegment] Color #${label} extraction failed:`,
            error instanceof Error ? error.message : String(error),
          );
        }
        return null;
      }
    },
  );

  const regions = regionResults.filter(
    (region): region is SegmentRegion => region != null,
  );

  regions.sort((a, b) => b.area - a.area);
  regions.forEach((reg, index) => {
    reg.id = index;
  });

  const finalRegions = regions.slice(0, maskCfg().maxRegionColors);
  const nameToId = new Map(finalRegions.map(reg => [reg.name, reg.id]));
  const pickBuildStart = __DEV__ ? performance.now() : 0;
  const { pick: pickBufferRaw, workAreas } = buildPickMapAndWorkAreas(
    layout.labels,
    indexToName,
    nameToId,
    baseboardBinary,
    cols,
    rows,
  );
  const pickBuffer = dilatePickBuffer1px(pickBufferRaw, cols, rows);
  for (const reg of finalRegions) {
    const workArea = workAreas.get(reg.name);
    if (workArea != null) {
      reg.area = workArea;
    }
  }

  return {
    regions: finalRegions,
    pickMap: { buffer: pickBuffer, cols, rows },
    labels: layout.labels,
    baseboardBinary,
    segCols: cols,
    segRows: rows,
  };
}

/** @deprecated Use extractRegionsFromMaskBuffer */
export async function extractRegionsFromMask(
  maskMat: WrappedMat,
  options: {
    minArea: number;
    approxEpsilon: number;
  },
): Promise<SegmentRegion[]> {
  const { buffer, cols, rows } = cv.matToBuffer(maskMat);
  const result = extractRegionsFromMaskBufferSync(buffer, cols, rows, options);
  return result.regions;
}
