import type { FunctionalComponent } from 'preact';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useAsukaStore } from '../../store/asukaStore';
import type { AsukaModuleComponent } from '../../core/modules';
import { registerModule } from '../../core/modules';
import { getI18n } from '../../core/i18n';
import { DEFAULT_PERMISSIONS, getEffectivePermissions } from '../../core/permissions';
import { convertPhysicalLength, type AsukaPhysicalSize } from '../../core/physicalMeasurement';
import { useT } from '../../hooks/useT';
import { getRuntimeRenderEngine } from '../../core/renderEngine';
import {
  listViewElementSources,
  measureViewElementSource,
  type AsukaViewElementGeometry,
} from '../../core/viewFromElement';
import {
  switchViewWithCanvas,
  getCurrentViewId,
  deleteSnapshotForView,
  saveSnapshotForView,
  unsetCurrentView,
} from '../../core/viewSnapshots';

import { Card, CardContent, Notice, Tabs } from '../core';
import { SelectField } from '../forms';
import { OverlayHost } from '../overlays/OverlayHost';
import {
  ViewEditorActions,
  ViewEditorFields,
  ViewListPanel,
  ViewsHeader,
  ViewSwitcherPanel,
  type AsukaView,
  type ViewEngineKind,
  type ViewFormValues,
  type ViewPreset,
  type ViewUnit,
} from '../domain/views';

const PRESETS: ViewPreset[] = [
  {
    id: 'custom',
    label: 'Custom',
    labelKey: 'views.presets.custom',
    unit: 'px',
    width: 800,
    height: 600,
    dpi: 96,
  },
  {
    id: 'a4',
    label: 'A4 (210×297 mm)',
    labelKey: 'views.presets.a4',
    unit: 'mm',
    width: 210,
    height: 297,
    dpi: 300,
  },
  {
    id: 'a5',
    label: 'A5 (148×210 mm)',
    labelKey: 'views.presets.a5',
    unit: 'mm',
    width: 148,
    height: 210,
    dpi: 300,
  },
  {
    id: 'letter',
    label: 'Letter (8.5×11 in)',
    labelKey: 'views.presets.letter',
    unit: 'in',
    width: 8.5,
    height: 11,
    dpi: 300,
  },
  {
    id: 'square30',
    label: 'Square (30×30 cm)',
    labelKey: 'views.presets.square30',
    unit: 'cm',
    width: 30,
    height: 30,
    dpi: 300,
  },
  {
    id: 'photo10x15',
    label: 'Photo (10×15 cm)',
    labelKey: 'views.presets.photo10x15',
    unit: 'cm',
    width: 10,
    height: 15,
    dpi: 300,
  },
  {
    id: 'ig1080',
    label: 'Instagram (1080×1080 px)',
    labelKey: 'views.presets.ig1080',
    unit: 'px',
    width: 1080,
    height: 1080,
    dpi: 96,
  },
  {
    id: 'hd',
    label: 'HD (1920×1080 px)',
    labelKey: 'views.presets.hd',
    unit: 'px',
    width: 1920,
    height: 1080,
    dpi: 96,
  },
];

function clampNumber(v: any, min: number, max: number): number {
  const n = Number(v);
  if (!Number.isFinite(n)) return min;
  return Math.max(min, Math.min(max, n));
}

function unitToPx(value: number, unit: ViewUnit, dpi: number): number {
  const v = Number(value) || 0;
  const d = Math.max(10, Number(dpi) || 96);
  if (unit === 'px') return v;
  if (unit === 'in') return v * d;
  if (unit === 'cm') return (v / 2.54) * d;
  if (unit === 'mm') return (v / 25.4) * d;
  return v;
}

function pxToUnit(px: number, unit: ViewUnit, dpi: number): number {
  const p = Number(px) || 0;
  const d = Math.max(10, Number(dpi) || 96);
  if (unit === 'px') return p;
  if (unit === 'in') return p / d;
  if (unit === 'cm') return (p / d) * 2.54;
  if (unit === 'mm') return (p / d) * 25.4;
  return p;
}

function createDefaultPhysicalSize(widthPx: number, heightPx: number, dpi: number): AsukaPhysicalSize {
  return {
    width: Number(pxToUnit(widthPx, 'cm', dpi).toFixed(4)),
    height: Number(pxToUnit(heightPx, 'cm', dpi).toFixed(4)),
    unit: 'cm',
    recommendedDpi: Math.max(1, Math.round(Number(dpi) || 150)),
  };
}

function normalizePhysicalSizeForm(input: unknown, fallback: AsukaPhysicalSize): AsukaPhysicalSize {
  const source = input && typeof input === 'object' ? input as AsukaPhysicalSize : fallback;
  const unit = source.unit === 'mm' || source.unit === 'in' ? source.unit : 'cm';
  const width = clampNumber(source.width ?? fallback.width, 0.01, 100000);
  const height = clampNumber(source.height ?? fallback.height, 0.01, 100000);
  const recommendedDpi = clampNumber(source.recommendedDpi ?? fallback.recommendedDpi ?? 150, 1, 1200);
  return {
    width,
    height,
    unit,
    recommendedDpi: Math.round(recommendedDpi),
    ...(source.variants && typeof source.variants === 'object' ? { variants: { ...source.variants } } : null),
    ...(String(source.activeVariantId ?? '').trim() ? { activeVariantId: String(source.activeVariantId).trim() } : null),
  };
}


function normalizeViewEngine(value: any, fallback: ViewEngineKind = 'fabric'): ViewEngineKind {
  const next = String(value ?? '').trim().toLowerCase();
  if (next === '3d') return '3d';
  if (next === 'fabric' || next === '2d') return 'fabric';
  return fallback;
}

function OverlayModal(props: { title: string; open: boolean; onClose: () => void; children: any }) {
  if (!props.open) return null;

  return (
    <OverlayHost type="modal" title={props.title} onClose={props.onClose} compactHeader>
      {props.children}
    </OverlayHost>
  );
}

function useViews() {
  return useAsukaStore((state: any) => (state.views ?? []) as AsukaView[]);
}

/**
 * ViewsModule
 * - Keeps original behaviors (select view, add view, immediate switch)
 * - Uses reusable domain/components for list, cards, switcher and editor fields.
 */
const ViewsModule: AsukaModuleComponent = ({ context }) => {
  const t = useT();
  const views = useViews();
  const designer = useAsukaStore((state: any) => state.designerInstance ?? null);
  const activeViewId = useAsukaStore((state: any) => String(state.activeViewId ?? ''));
  const defaultRenderEngineKind = useAsukaStore((state: any) => state.defaultRenderEngineKind ?? 'fabric');
  const addView = useAsukaStore((state: any) => state.addView);
  const updateView = useAsukaStore((state: any) => state.updateView);
  const removeView = useAsukaStore((state: any) => state.removeView);
  const elements = useAsukaStore((state: any) => state.elements ?? []);
  const permissionState = useAsukaStore((state: any) => ({
    permissions: state.permissions ?? DEFAULT_PERMISSIONS,
    permissionsByView: state.permissionsByView ?? {},
  }));

  // Empty-state CTA (no views) reuses this module's create form.
  const pendingOpenCreate = useAsukaStore((state: any) => state.pendingOpenCreateViewModal);
  const clearPendingOpenCreate = useAsukaStore(
    (state: any) => state.clearPendingOpenCreateViewModal,
  );

  const [modalOpen, setModalOpen] = useState(false);
  const [creationSourceMode, setCreationSourceMode] = useState<'blank' | 'element'>('blank');
  const [selectedElementId, setSelectedElementId] = useState('');
  const [selectedElementGeometry, setSelectedElementGeometry] = useState<AsukaViewElementGeometry | null>(null);
  const [elementMeasureState, setElementMeasureState] = useState<'idle' | 'loading' | 'ready' | 'error'>('idle');
  const [viewCreationError, setViewCreationError] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);
  const elementMeasureRequest = useRef(0);
  const [form, setForm] = useState<ViewFormValues>({
    mode: 'create',
    name: '',
    engine: 'fabric',
    unit: 'px',
    width: 800,
    height: 600,
    dpi: 96,
    presetId: 'custom',
    physicalEnabled: false,
    physicalSize: createDefaultPhysicalSize(800, 600, 96),
  });

  const effectiveActiveId = useMemo(() => {
    const fromStore = String(activeViewId ?? '').trim();
    if (fromStore) return fromStore;
    const current = getCurrentViewId();
    return current ?? views?.[0]?.id ?? '';
  }, [activeViewId, views]);

  const elementSources = useMemo(() => listViewElementSources(elements), [elements]);
  const elementSourceById = useMemo(
    () => new Map(elementSources.map((source) => [source.id, source])),
    [elementSources],
  );
  const elementOptions = useMemo(() => [
    { value: '', label: t('views.fromElement.choose', 'Choose an element') },
    ...elementSources.map((source) => ({ value: source.id, label: source.name })),
  ], [elementSources, t]);

  const permissions = useMemo(
    () =>
      getEffectivePermissions(
        (permissionState.permissions ?? DEFAULT_PERMISSIONS) as any,
        (permissionState.permissionsByView?.[effectiveActiveId] ?? null) as any,
      ),
    [effectiveActiveId, permissionState.permissions, permissionState.permissionsByView],
  );

  const canCreateView =
    Boolean((permissions as any)?.canCreateView ?? true) &&
    Boolean((permissions as any)?.canCreateView ?? true);
  const canUseElements = Boolean((permissions as any)?.canViewElements ?? true);
  const canEditView = Boolean((permissions as any)?.canEditView ?? true);
  const canDeleteView = Boolean((permissions as any)?.canDeleteView ?? true);

  const getPermissionsForView = useCallback(
    (viewId: string) =>
      getEffectivePermissions(
        (permissionState.permissions ?? DEFAULT_PERMISSIONS) as any,
        (permissionState.permissionsByView?.[String(viewId ?? '')] ?? null) as any,
      ),
    [permissionState.permissions, permissionState.permissionsByView],
  );

  const canEditSpecificView = useCallback(
    (viewId: string) => Boolean((getPermissionsForView(viewId) as any)?.canEditView ?? true),
    [getPermissionsForView],
  );

  const canDeleteSpecificView = useCallback(
    (viewId: string) => Boolean((getPermissionsForView(viewId) as any)?.canDeleteView ?? true),
    [getPermissionsForView],
  );

  const viewOptions = useMemo(
    () =>
      (views ?? []).map((view) => {
        const engine = normalizeViewEngine((view as any).engine, normalizeViewEngine(defaultRenderEngineKind, 'fabric'));
        const label = view.label ?? view.name ?? view.id;
        return {
          value: view.id,
          label: `${label} · ${engine === '3d' ? '3D' : '2D'}`,
        };
      }),
    [defaultRenderEngineKind, views],
  );

  const handleSelectView = useCallback(
    (viewId: string) => {
      if (String(viewId) === String(effectiveActiveId)) return;
      switchViewWithCanvas(designer, viewId);
    },
    [designer, effectiveActiveId],
  );

  const handleDeleteView = useCallback(
    (viewId: string) => {
      const id = String(viewId ?? '').trim();
      if (!id || !canDeleteView || !canDeleteSpecificView(id)) return;
      const label = (views ?? []).find((view) => view.id === id)?.name ?? id;
      if (typeof window !== 'undefined') {
        const ok = window.confirm(`Delete view "${label}"? This cannot be undone.`);
        if (!ok) return;
      }

      // If deleting current view, switch first to a safe target BEFORE deleting snapshot.
      const current = effectiveActiveId;
      const nextTarget = (views ?? []).find((view) => view.id !== id)?.id ?? '';
      if (id === current && nextTarget) {
        try {
          switchViewWithCanvas(designer, nextTarget);
        } catch {}
      } else if (id === current && !nextTarget) {
        // Removing the last remaining view: clear canvas and unset current/active view.
        try {
          unsetCurrentView(designer);
        } catch {}
      }

      try {
        deleteSnapshotForView(id);
      } catch {}
      try {
        removeView?.(id);
      } catch {}
    },
    [canDeleteSpecificView, canDeleteView, designer, effectiveActiveId, removeView, views],
  );

  const handleElementSourceChange = useCallback(async (elementId: string) => {
    const id = String(elementId ?? '').trim();
    const requestId = ++elementMeasureRequest.current;
    setSelectedElementId(id);
    setSelectedElementGeometry(null);
    setViewCreationError('');
    if (!id) {
      setElementMeasureState('idle');
      return;
    }

    const source = elementSourceById.get(id);
    if (!source) {
      setElementMeasureState('error');
      setViewCreationError(t('views.fromElement.unavailable', 'This element does not contain a usable 2D image.'));
      return;
    }

    setElementMeasureState('loading');
    const measured = await measureViewElementSource(source);
    if (requestId !== elementMeasureRequest.current) return;
    if (!measured) {
      setElementMeasureState('error');
      setViewCreationError(t('views.fromElement.measureFailed', 'The image dimensions could not be detected.'));
      return;
    }

    setSelectedElementGeometry(measured);
    setElementMeasureState('ready');
    setForm((current) => ({
      ...current,
      name: measured.name,
      engine: 'fabric',
      unit: 'px',
      width: measured.width,
      height: measured.height,
      dpi: 96,
      presetId: 'custom',
      physicalEnabled: false,
      physicalSize: createDefaultPhysicalSize(measured.width, measured.height, 96),
    }));
  }, [elementSourceById, t]);

  const openCreateModal = useCallback(() => {
    if (!canCreateView) return;
    // Default to active view meta when available.
    const active = (views ?? []).find((view) => view.id === effectiveActiveId) ?? (views ?? [])[0];
    const unit: ViewUnit = (active?.unit as any) || 'px';
    const engine = normalizeViewEngine((active as any)?.engine, normalizeViewEngine(defaultRenderEngineKind, 'fabric'));
    const dpi = clampNumber(active?.dpi ?? 96, 10, 1200);
    const width = clampNumber(pxToUnit(Number(active?.width ?? 800), unit, dpi), 1, 100000);
    const height = clampNumber(pxToUnit(Number(active?.height ?? 600), unit, dpi), 1, 100000);
    const defaultPhysicalSize = createDefaultPhysicalSize(Number(active?.width ?? 800), Number(active?.height ?? 600), dpi);
    setForm({
      mode: 'create',
      name: '',
      engine,
      unit,
      dpi,
      width,
      height,
      presetId: 'custom',
      physicalEnabled: Boolean(active?.physicalSize),
      physicalSize: normalizePhysicalSizeForm(active?.physicalSize, defaultPhysicalSize),
    });
    elementMeasureRequest.current += 1;
    setCreationSourceMode('blank');
    setSelectedElementId('');
    setSelectedElementGeometry(null);
    setElementMeasureState('idle');
    setViewCreationError('');
    setIsSubmitting(false);
    setModalOpen(true);
  }, [canCreateView, defaultRenderEngineKind, effectiveActiveId, views]);

  useEffect(() => {
    if (!pendingOpenCreate) return;
    // Open the existing create view form, then clear the pending flag.
    if (canCreateView) openCreateModal();
    clearPendingOpenCreate?.();
  }, [pendingOpenCreate, openCreateModal, clearPendingOpenCreate, canCreateView]);

  useEffect(() => {
    if (!modalOpen) return;
    if (form.mode === 'create' && !canCreateView) {
      setModalOpen(false);
      return;
    }
    if (
      form.mode === 'edit' &&
      (!canEditView || (form.viewId && !canEditSpecificView(form.viewId)))
    ) {
      setModalOpen(false);
    }
  }, [canCreateView, canEditSpecificView, canEditView, form.mode, form.viewId, modalOpen]);

  const openEditModal = useCallback(
    (viewId: string) => {
      if (!canEditView || !canEditSpecificView(viewId)) return;
      const view = (views ?? []).find((item) => item.id === viewId);
      if (!view) return;
      const unit: ViewUnit = (view.unit as any) || 'px';
      const engine = normalizeViewEngine((view as any).engine, normalizeViewEngine(defaultRenderEngineKind, 'fabric'));
      const dpi = clampNumber(view.dpi ?? 96, 10, 1200);
      const width = clampNumber(pxToUnit(Number(view.width ?? 800), unit, dpi), 1, 100000);
      const height = clampNumber(pxToUnit(Number(view.height ?? 600), unit, dpi), 1, 100000);
      const defaultPhysicalSize = createDefaultPhysicalSize(Number(view.width ?? 800), Number(view.height ?? 600), dpi);
      setForm({
        mode: 'edit',
        viewId,
        name: String(view.name ?? ''),
        engine,
        unit,
        dpi,
        width,
        height,
        presetId: 'custom',
        physicalEnabled: Boolean(view.physicalSize),
        physicalSize: normalizePhysicalSizeForm(view.physicalSize, defaultPhysicalSize),
      });
      elementMeasureRequest.current += 1;
      setCreationSourceMode('blank');
      setSelectedElementId('');
      setSelectedElementGeometry(null);
      setElementMeasureState('idle');
      setViewCreationError('');
      setIsSubmitting(false);
      setModalOpen(true);
    },
    [canEditSpecificView, canEditView, defaultRenderEngineKind, views],
  );

  const applyPreset = useCallback((presetId: string) => {
    const preset = PRESETS.find((item) => item.id === presetId);
    if (!preset) return;
    setForm((prev) => {
      const isPhysicalPreset = preset.unit !== 'px';
      return {
        ...prev,
        presetId,
        unit: preset.unit,
        width: preset.width,
        height: preset.height,
        dpi: preset.dpi,
        physicalEnabled: isPhysicalPreset,
        physicalSize: isPhysicalPreset
          ? {
              ...prev.physicalSize,
              width: preset.width,
              height: preset.height,
              unit: preset.unit as AsukaPhysicalSize['unit'],
              recommendedDpi: preset.dpi,
            }
          : prev.physicalSize,
      };
    });
  }, []);

  const handleUnitChange = useCallback((nextUnit: ViewUnit) => {
    setForm((prev) => {
      const dpi = clampNumber(prev.dpi, 10, 1200);
      const curWpx = unitToPx(prev.width, prev.unit, dpi);
      const curHpx = unitToPx(prev.height, prev.unit, dpi);
      return {
        ...prev,
        unit: nextUnit,
        width: clampNumber(pxToUnit(curWpx, nextUnit, dpi), 1, 100000),
        height: clampNumber(pxToUnit(curHpx, nextUnit, dpi), 1, 100000),
        presetId: 'custom',
      };
    });
  }, []);

  const handlePhysicalEnabledChange = useCallback((enabled: boolean) => {
    setForm((prev) => {
      if (!enabled || prev.physicalEnabled) return { ...prev, physicalEnabled: enabled };
      const widthPx = unitToPx(prev.width, prev.unit, prev.dpi);
      const heightPx = unitToPx(prev.height, prev.unit, prev.dpi);
      return {
        ...prev,
        physicalEnabled: true,
        physicalSize: normalizePhysicalSizeForm(
          prev.physicalSize,
          createDefaultPhysicalSize(widthPx, heightPx, prev.dpi),
        ),
      };
    });
  }, []);

  const handlePhysicalSizeChange = useCallback((patch: Partial<AsukaPhysicalSize>) => {
    setForm((prev) => {
      const current = prev.physicalSize;
      const next = { ...current } as AsukaPhysicalSize;
      if (patch.unit === 'mm' || patch.unit === 'cm' || patch.unit === 'in') {
        if (patch.unit !== current.unit) {
          next.width = convertPhysicalLength(current.width, current.unit, patch.unit);
          next.height = convertPhysicalLength(current.height, current.unit, patch.unit);
        }
        next.unit = patch.unit;
      }
      if (Object.prototype.hasOwnProperty.call(patch, 'width')) next.width = clampNumber(patch.width, 0.01, 100000);
      if (Object.prototype.hasOwnProperty.call(patch, 'height')) next.height = clampNumber(patch.height, 0.01, 100000);
      if (Object.prototype.hasOwnProperty.call(patch, 'recommendedDpi')) {
        next.recommendedDpi = Math.round(clampNumber(patch.recommendedDpi, 1, 1200));
      }
      return { ...prev, physicalSize: next };
    });
  }, []);

  const handleSubmit = useCallback(async () => {
    const name = String(form.name ?? '').trim();
    const unit = form.unit;
    const engine = normalizeViewEngine((form as any).engine, normalizeViewEngine(defaultRenderEngineKind, 'fabric'));
    const dpi = clampNumber(form.dpi, 10, 1200);
    const wUnit = clampNumber(form.width, 1, 100000);
    const hUnit = clampNumber(form.height, 1, 100000);
    const widthPx = Math.max(1, Math.round(unitToPx(wUnit, unit, dpi)));
    const heightPx = Math.max(1, Math.round(unitToPx(hUnit, unit, dpi)));
    const physicalSize = form.physicalEnabled
      ? normalizePhysicalSizeForm(form.physicalSize, createDefaultPhysicalSize(widthPx, heightPx, dpi))
      : undefined;

    setViewCreationError('');
    setIsSubmitting(true);
    try {
      if (form.mode === 'edit' && form.viewId && typeof updateView === 'function') {
        if (!canEditView || !canEditSpecificView(form.viewId)) return;
        updateView(form.viewId, {
          name: name || 'View',
          width: widthPx,
          height: heightPx,
          dpi,
          unit,
          engine,
          physicalSize,
        });
        // If editing the active view, SAVE current canvas first then reload with new dimensions.
        if (form.viewId === effectiveActiveId) {
          try {
            saveSnapshotForView(designer, form.viewId);
          } catch {}
          await switchViewWithCanvas(designer, form.viewId);
        }
        setModalOpen(false);
        return;
      }

      if (!canCreateView || typeof addView !== 'function') return;
      const source = creationSourceMode === 'element' ? selectedElementGeometry : null;
      if (creationSourceMode === 'element' && (!source || elementMeasureState !== 'ready')) {
        setViewCreationError(t('views.fromElement.chooseFirst', 'Choose an element before creating the view.'));
        return;
      }

      const previousViewId = effectiveActiveId;
      const id = addView({ name: name || undefined, width: widthPx, height: heightPx, dpi, unit, engine, physicalSize });
      if (!id) return;

      try {
        await switchViewWithCanvas(designer, id);
        if (source) {
          const fabricEngine = getRuntimeRenderEngine('fabric') ?? context.getEngine?.();
          if (fabricEngine?.kind !== 'fabric') throw new Error(t('views.fromElement.rendererUnavailable', 'The 2D renderer is not ready.'));
          const added = await fabricEngine.objects.addElement({
            element: source.element,
            variation: source.variation,
            variationId: source.variationId,
            fitMode: 'native',
            at: { x: widthPx / 2, y: heightPx / 2 },
            source: 'module.views.create-from-element',
          });
          if (!added?.changed) throw new Error(t('views.fromElement.addFailed', 'The element could not be added to the new view.'));
          try { await fabricEngine.history.commit({ reason: 'view-created-from-element', force: true }); } catch {}
          saveSnapshotForView(designer, id);
        }
        setModalOpen(false);
      } catch (error: any) {
        try { deleteSnapshotForView(id); } catch {}
        try { removeView?.(id); } catch {}
        try {
          if (previousViewId) await switchViewWithCanvas(designer, previousViewId);
          else unsetCurrentView(designer);
        } catch {}
        setViewCreationError(String(error?.message ?? t('views.fromElement.createFailed', 'Unable to create this view from the element.')));
      }
    } finally {
      setIsSubmitting(false);
    }
  }, [
    addView,
    canCreateView,
    canEditSpecificView,
    canEditView,
    context,
    creationSourceMode,
    designer,
    defaultRenderEngineKind,
    elementMeasureState,
    effectiveActiveId,
    form,
    removeView,
    selectedElementGeometry,
    t,
    updateView,
  ]);

  return (
    <Card className="ask-overflow-hidden">
      <ViewsHeader
        count={(views ?? []).length}
        hasActiveView={Boolean(effectiveActiveId)}
        canAdd={canCreateView && typeof addView === 'function'}
        onAdd={openCreateModal}
      />

      <CardContent className="ask-flex ask-flex-col ask-gap-4 ask-pt-4">
        {viewOptions.length > 0 ? (
          <ViewSwitcherPanel
            views={views ?? []}
            activeId={effectiveActiveId}
            options={viewOptions}
            onSelect={handleSelectView}
          />
        ) : null}

        <ViewListPanel
          views={views ?? []}
          activeId={effectiveActiveId}
          canAdd={canCreateView && typeof addView === 'function'}
          canEdit={(viewId) => canEditView && canEditSpecificView(viewId)}
          canDelete={(viewId) =>
            canDeleteView && canDeleteSpecificView(viewId) && typeof removeView === 'function'
          }
          onAdd={openCreateModal}
          onSelect={handleSelectView}
          onEdit={openEditModal}
          onDelete={handleDeleteView}
        />
      </CardContent>

      <OverlayModal
        open={modalOpen}
        onClose={() => setModalOpen(false)}
        title={form.mode === 'edit' ? t('common.edit', 'Edit view') : t('ui.add_view', 'Add view')}
      >
        <div className="ask-flex ask-flex-col ask-gap-4">
          {form.mode === 'create' ? (
            <div className="ask-flex ask-flex-col ask-gap-3">
              <Tabs
                value={creationSourceMode}
                onChange={(mode) => {
                  setCreationSourceMode(mode === 'element' ? 'element' : 'blank');
                  setViewCreationError('');
                }}
                items={[
                  { id: 'blank', label: t('views.blankView', 'Blank view') },
                  { id: 'element', label: t('views.fromElement', 'From element'), disabled: !canUseElements },
                ]}
              />

              {creationSourceMode === 'element' ? (
                <div className="ask-flex ask-flex-col ask-gap-3">
                  <SelectField
                    label={t('views.fromElement.label', 'Element')}
                    hint={t('views.fromElement.hint', 'The view uses the image’s native pixel dimensions, like opening an image in an editor.')}
                    value={selectedElementId}
                    options={elementOptions}
                    disabled={!elementSources.length || isSubmitting}
                    onValueChange={(value) => { void handleElementSourceChange(value); }}
                  />

                  {!elementSources.length ? (
                    <Notice variant="neutral">
                      {t('views.fromElement.empty', 'Create a 2D element with an image before using this option.')}
                    </Notice>
                  ) : null}
                  {elementMeasureState === 'loading' ? (
                    <Notice variant="neutral">{t('views.fromElement.measuring', 'Reading image dimensions…')}</Notice>
                  ) : null}
                  {elementMeasureState === 'ready' && selectedElementGeometry ? (
                    <Notice variant="primary">
                      {t(
                        'views.fromElement.ready',
                        'The view will be {width} × {height} px and the element will keep its native size.',
                        { width: selectedElementGeometry.width, height: selectedElementGeometry.height },
                      )}
                    </Notice>
                  ) : null}
                </div>
              ) : null}
            </div>
          ) : null}

          {viewCreationError ? <Notice variant="danger">{viewCreationError}</Notice> : null}

          <ViewEditorFields
            form={form}
            presets={PRESETS}
            onPresetChange={applyPreset}
            onEngineChange={(engine) => setForm((prev) => ({ ...prev, engine }))}
            onNameChange={(name) => setForm((prev) => ({ ...prev, name }))}
            onUnitChange={handleUnitChange}
            onDpiChange={(dpi) =>
              setForm((prev) => ({ ...prev, dpi: clampNumber(dpi, 10, 1200), presetId: 'custom' }))
            }
            onWidthChange={(width) =>
              setForm((prev) => ({
                ...prev,
                width: clampNumber(width, 1, 100000),
                presetId: 'custom',
              }))
            }
            onHeightChange={(height) =>
              setForm((prev) => ({
                ...prev,
                height: clampNumber(height, 1, 100000),
                presetId: 'custom',
              }))
            }
            onPhysicalEnabledChange={handlePhysicalEnabledChange}
            onPhysicalSizeChange={handlePhysicalSizeChange}
            geometryLocked={form.mode === 'create' && creationSourceMode === 'element'}
            t={t}
          />

          <ViewEditorActions
            mode={form.mode}
            onCancel={() => setModalOpen(false)}
            onSubmit={handleSubmit}
            disabled={form.mode === 'create' && creationSourceMode === 'element' && elementMeasureState !== 'ready'}
            busy={isSubmitting}
            t={t}
          />
        </div>
      </OverlayModal>
    </Card>
  );
};

/**
 * Enregistrement du module Views dans le registre global.
 */
registerModule({
  id: 'views' as any,
  label: getI18n().t('modules.views'),
  icon: 'views',
  displayMode: 'floating',
  supportedEngines: ['fabric', '3d'],
  render: ViewsModule as unknown as FunctionalComponent<any>,
});

export default ViewsModule;
