
import { fabric } from '../fabric';

export function createClipPathsRuntime(params: {
  canvas: any;
  getZoneClipRect: (zone: any) => any;
  getZoneForObject: (obj: any) => any;
  getPolicy: (zoneId: string) => any;
  isConstraintsEnabled: () => boolean;
  listRuntimeObjects: () => any[];
}) {
  const { canvas, getZoneClipRect, getZoneForObject, getPolicy, isConstraintsEnabled, listRuntimeObjects } = params;
  const clipCache = new Map<string, any>();
  const zoneSvgClipLoading = new Set<string>();

  const getZoneClipCacheKey = (zone: any, r: any) => `${zone.id}|${String(zone?.shape ?? 'rect')}|${r?._insetKey ?? ''}|${Number(zone?.left ?? 0)}|${Number(zone?.top ?? 0)}|${Number(zone?.width ?? 0)}|${Number(zone?.height ?? 0)}`;

  const createZoneClipObjectSync = (zone: any, r: any): any | null => {
    const shape = String(zone?.shape ?? 'rect');
    if (shape === 'ellipse') {
      return new fabric.Ellipse({ left: r.left + r.width / 2, top: r.top + r.height / 2, rx: Math.max(1, r.width / 2), ry: Math.max(1, r.height / 2), originX: 'center', originY: 'center', absolutePositioned: true });
    }
    if (shape === 'triangle') {
      return new fabric.Triangle({ left: r.left + r.width / 2, top: r.top + r.height / 2, width: Math.max(1, r.width), height: Math.max(1, r.height), originX: 'center', originY: 'center', absolutePositioned: true });
    }
    if (shape === 'star') {
      const outer = Math.max(1, Math.min(r.width, r.height) / 2);
      const inner = Math.max(1, outer * 0.45);
      const pts: Array<{ x: number; y: number }> = [];
      let rot = (Math.PI / 2) * 3;
      const step = Math.PI / 5;
      for (let i = 0; i < 5; i++) {
        pts.push({ x: Math.cos(rot) * outer, y: Math.sin(rot) * outer });
        rot += step;
        pts.push({ x: Math.cos(rot) * inner, y: Math.sin(rot) * inner });
        rot += step;
      }
      return new fabric.Polygon(pts, { left: r.left + r.width / 2, top: r.top + r.height / 2, originX: 'center', originY: 'center', absolutePositioned: true, scaleX: r.width / Math.max(outer * 2, 1), scaleY: r.height / Math.max(outer * 2, 1) } as any);
    }
    return new fabric.Rect({
      left: r.left,
      top: r.top,
      width: r.width,
      height: r.height,
      originX: 'left',
      originY: 'top',
      absolutePositioned: true,
    });
  };

  const normalizeSvgObjectToRect = (obj: any, r: any) => {
    if (!obj || !r) return obj;
    try { obj.set?.({ absolutePositioned: true }); } catch {}
    try { obj.setCoords?.(); } catch {}
    try {
      const br = obj.getBoundingRect?.(true, true);
      if (br?.width && br?.height) {
        const sx = Number(r.width ?? 1) / Math.max(Number(br.width) || 1, 1);
        const sy = Number(r.height ?? 1) / Math.max(Number(br.height) || 1, 1);
        obj.scaleX = (Number(obj.scaleX ?? 1) || 1) * sx;
        obj.scaleY = (Number(obj.scaleY ?? 1) || 1) * sy;
        obj.setCoords?.();
      }
    } catch {}
    try {
      const br2 = obj.getBoundingRect?.(true, true);
      if (br2) {
        const dx = Number(r.left ?? 0) - Number(br2.left ?? 0);
        const dy = Number(r.top ?? 0) - Number(br2.top ?? 0);
        obj.set?.({ left: Number(obj.left ?? 0) + dx, top: Number(obj.top ?? 0) + dy });
        obj.setCoords?.();
      }
    } catch {}
    return obj;
  };

  const reapplyZoneClips = (zoneId: string) => {
    try {
      for (const obj of listRuntimeObjects()) {
        if (String((obj as any)?._asukaZoneId ?? '') === String(zoneId)) applyClipForObject(obj);
      }
      canvas.requestRenderAll?.();
    } catch {}
  };

  const ensureSvgClipForZoneAsync = (zone: any, r: any, key: string) => {
    const svgText = typeof zone?.sourceSvg === 'string' ? String(zone.sourceSvg) : '';
    if (!svgText || clipCache.has(key) || zoneSvgClipLoading.has(key)) return;
    zoneSvgClipLoading.add(key);
    try {
      const load = (fabric as any).loadSVGFromString?.(svgText);
      const finalize = (res: any) => {
        try {
          const rawObjects = Array.isArray(res) ? (res[0] ?? []) : (res?.objects ?? []);
          const objects = Array.isArray(rawObjects) ? rawObjects.filter(Boolean) : (rawObjects ? [rawObjects] : []);
          const options = Array.isArray(res) ? (res[1] ?? {}) : (res?.options ?? {});
          let clipObj: any = null;
          const groupFn = (fabric as any).util?.groupSVGElements;
          if (objects.length === 1) clipObj = objects[0];
          if (!clipObj && typeof groupFn === 'function') { try { clipObj = groupFn(objects, options || {}); } catch {} }
          if (!clipObj && typeof (fabric as any).Group === 'function') { try { clipObj = new (fabric as any).Group(objects, options || {}); } catch {} }
          if (!clipObj) return;
          try { clipObj.set({ originX: 'left', originY: 'top', left: Number(r.left ?? 0), top: Number(r.top ?? 0), absolutePositioned: true }); } catch {}
          normalizeSvgObjectToRect(clipObj, r);
          (clipObj as any)._asukaRole = 'clipPath';
          (clipObj as any).itemGroup = (clipObj as any).itemGroup ?? 'Tools';
          clipCache.set(key, clipObj);
          reapplyZoneClips(zone.id);
        } finally {
          zoneSvgClipLoading.delete(key);
        }
      };
      if (load && typeof load.then === 'function') load.then((res: any) => finalize(res)).catch(() => zoneSvgClipLoading.delete(key));
      else (fabric as any).loadSVGFromString(svgText, (objects: any[], options: any) => finalize({ objects, options }));
    } catch {
      zoneSvgClipLoading.delete(key);
    }
  };

  const getClipForZone = (zone: any) => {
    const r = getZoneClipRect(zone);
    const key = getZoneClipCacheKey(zone, r);
    const cached = clipCache.get(key);
    if (cached) {
      try {
        const shape = String(zone?.shape ?? 'rect');
        if (shape === 'ellipse') cached.set({ left: r.left + r.width / 2, top: r.top + r.height / 2, rx: Math.max(1, r.width / 2), ry: Math.max(1, r.height / 2) });
        else if (shape === 'triangle') cached.set({ left: r.left + r.width / 2, top: r.top + r.height / 2, width: r.width, height: r.height });
        else if (shape === 'star') cached.set({ left: r.left + r.width / 2, top: r.top + r.height / 2, scaleX: r.width / Math.max(Math.min(r.width, r.height), 1), scaleY: r.height / Math.max(Math.min(r.width, r.height), 1) });
        else if (shape === 'svg') normalizeSvgObjectToRect(cached, r);
        else cached.set({ left: r.left, top: r.top, width: r.width, height: r.height });
        cached.setCoords?.();
      } catch {}
      return cached;
    }
    if (String(zone?.shape ?? 'rect') === 'svg' && typeof zone?.sourceSvg === 'string' && zone.sourceSvg) {
      ensureSvgClipForZoneAsync(zone, r, key);
      const fallback = createZoneClipObjectSync({ ...(zone as any), shape: 'rect' } as any, r);
      if (fallback) {
        (fallback as any)._asukaRole = 'clipPath';
        (fallback as any).itemGroup = (fallback as any).itemGroup ?? 'Tools';
        return fallback;
      }
      return null;
    }
    const clip = createZoneClipObjectSync(zone, r);
    if (!clip) return null;
    (clip as any)._asukaRole = 'clipPath';
    (clip as any).itemGroup = (clip as any).itemGroup ?? 'Tools';
    clipCache.set(key, clip);
    return clip;
  };

  const applyClipForObject = (obj: any) => {
    if (!obj) return;
    if (!isConstraintsEnabled()) {
      try { obj.clipPath = null; } catch {}
      return;
    }
    if ((obj as any)._asukaElementIsInstance || (obj as any)._asukaElementName) {
      try { obj.clipPath = null; } catch {}
      return;
    }
    if ((obj as any)._asukaIgnoreDrawingZones) {
      try { obj.clipPath = null; } catch {}
      return;
    }
    const role = (obj as any)._asukaRole;
    if (role === 'drawingZone' || role === 'snapVertical' || role === 'snapHorizontal' || role === 'clipPath') return;
    const zone = getZoneForObject(obj);
    try {
      const policy = getPolicy(zone.id);

      // Drawing areas clip by default: this is the expected production behavior.
      // Movement constraints stay permissive, so objects may partially overflow,
      // but their visible pixels are clipped to the area unless disabled explicitly.
      if (policy?.clipping?.enabled === false) {
        obj.clipPath = null;
      } else {
        obj.clipPath = getClipForZone(zone);
      }
    } catch {}
  };

  const clearCache = () => {
    clipCache.clear();
    zoneSvgClipLoading.clear();
  };

  return {
    createZoneClipObjectSync,
    normalizeSvgObjectToRect,
    getClipForZone,
    applyClipForObject,
    reapplyZoneClips,
    clearCache,
    reset: clearCache,
    getState: () => ({ clipCache, zoneSvgClipLoading }),
  };
}
