import type { EffectInstance, NPoint } from './types/effects';

type FieldEffectInstance = Extract<EffectInstance, { type: 'fog' | 'fire' | 'rain' | 'water' }>;

function distSqPointToSegment(
  px: number,
  py: number,
  x1: number,
  y1: number,
  x2: number,
  y2: number,
): number {
  const dx = x2 - x1;
  const dy = y2 - y1;
  const len2 = dx * dx + dy * dy;
  if (len2 < 1e-18) {
    const ex = px - x1;
    const ey = py - y1;
    return ex * ex + ey * ey;
  }
  let t = ((px - x1) * dx + (py - y1) * dy) / len2;
  t = Math.max(0, Math.min(1, t));
  const qx = x1 + t * dx;
  const qy = y1 + t * dy;
  const ex = px - qx;
  const ey = py - qy;
  return ex * ex + ey * ey;
}

function minDistToEraserStroke(
  p: { x: number; y: number },
  eraserPoints: readonly { x: number; y: number }[],
): number {
  if (eraserPoints.length === 0) return Number.POSITIVE_INFINITY;
  let bestSq = Number.POSITIVE_INFINITY;
  for (const ep of eraserPoints) {
    const dx = p.x - ep.x;
    const dy = p.y - ep.y;
    bestSq = Math.min(bestSq, dx * dx + dy * dy);
  }
  for (let i = 1; i < eraserPoints.length; i += 1) {
    const a = eraserPoints[i - 1];
    const b = eraserPoints[i];
    if (!a || !b) continue;
    bestSq = Math.min(bestSq, distSqPointToSegment(p.x, p.y, a.x, a.y, b.x, b.y));
  }
  return Math.sqrt(bestSq);
}

/**
 * `inclusive` — ластик: радиус кисти + радиус инстанса (снять всё пятно).
 * `brush` — вода/дождь по огню: укороченный радиус кисти (спрайты огня шире radiusN, иначе гасится «издалека»).
 */
export type FieldEraseHitMode = 'inclusive' | 'brush';

/** Доля радиуса кисти для гашения огня водой/дождём (центр кисти ближе к точке огня). */
export const FIRE_EXTINGUISH_BRUSH_SCALE = 0.38;

function eraseThresholdN(
  eraserRadiusN: number,
  instRadiusN: number,
  hitMode: FieldEraseHitMode,
): number {
  if (hitMode === 'brush') {
    return Math.max(0.006, eraserRadiusN * FIRE_EXTINGUISH_BRUSH_SCALE);
  }
  return eraserRadiusN + instRadiusN;
}

function isPointErased(
  p: { x: number; y: number },
  instRadiusN: number,
  eraserPoints: readonly { x: number; y: number }[],
  eraserRadiusN: number,
  hitMode: FieldEraseHitMode,
): boolean {
  const threshold = eraseThresholdN(eraserRadiusN, instRadiusN, hitMode);
  return minDistToEraserStroke(p, eraserPoints) <= threshold;
}

function splitKeptPointRuns(
  points: NPoint[],
  eraserPoints: readonly { x: number; y: number }[],
  eraserRadiusN: number,
  instRadiusN: number,
  hitMode: FieldEraseHitMode,
): NPoint[][] {
  const runs: NPoint[][] = [];
  let current: NPoint[] = [];
  for (const p of points) {
    if (isPointErased(p, instRadiusN, eraserPoints, eraserRadiusN, hitMode)) {
      if (current.length > 0) {
        runs.push(current);
        current = [];
      }
      continue;
    }
    current.push(p);
  }
  if (current.length > 0) runs.push(current);
  return runs;
}

function isFieldEffect(inst: EffectInstance): inst is FieldEffectInstance {
  return inst.type === 'fog' || inst.type === 'fire' || inst.type === 'rain' || inst.type === 'water';
}

export type FieldEffectType = FieldEffectInstance['type'];

/** Стирает эффекты поля (туман/дождь/огонь/вода) кистью, как «Кисть Открытия» для затемнения. */
export function applyFieldEffectEraserStroke(
  instances: readonly EffectInstance[],
  eraserPoints: readonly NPoint[],
  eraserRadiusN: number,
  makeId: (prefix: string) => string,
  targetTypes?: readonly FieldEffectType[],
  hitMode: FieldEraseHitMode = 'inclusive',
): EffectInstance[] {
  if (eraserPoints.length === 0) return [...instances];
  const typeFilter = targetTypes && targetTypes.length > 0 ? new Set(targetTypes) : null;

  const out: EffectInstance[] = [];
  for (const inst of instances) {
    if (!isFieldEffect(inst)) {
      out.push(inst);
      continue;
    }
    if (typeFilter && !typeFilter.has(inst.type)) {
      out.push(inst);
      continue;
    }

    const runs = splitKeptPointRuns(inst.points, eraserPoints, eraserRadiusN, inst.radiusN, hitMode);
    if (runs.length === 0) continue;
    if (runs.length === 1 && runs[0]?.length === inst.points.length) {
      out.push(inst);
      continue;
    }

    for (const run of runs) {
      if (run.length === 0) continue;
      out.push({
        ...inst,
        id: makeId(inst.type),
        points: run,
      });
    }
  }
  return out;
}
