"use client"; import { createContext, useContext, useState, type ReactNode, type SetStateAction, } from "react"; import { useStore } from "zustand"; import { createStore, type StoreApi } from "zustand/vanilla"; import type { EntityGeometriesSearchItem } from "@/uhm/api/geometries"; import type { Wiki } from "@/uhm/api/wikis"; import type { FeatureCollection, FeatureId } from "@/uhm/types/geo"; import type { Entity, EntitySnapshot } from "@/uhm/types/entities"; import type { EntityWikiLinkSnapshot, EditorSnapshot, Project, ProjectCommit, ProjectState } from "@/uhm/types/projects"; import type { WikiSnapshot } from "@/uhm/types/wiki"; import type { EditorMode, EntityFormState, GeometryMetaFormState, TimelineRange, } from "@/uhm/lib/editor/session/sessionTypes"; import type { UnifiedSearchKind } from "@/uhm/components/ui/UnifiedSearchBar"; import { HIDDEN_BACKGROUND_LAYER_VISIBILITY, type BackgroundLayerVisibility, } from "@/uhm/lib/map/styles/backgroundLayers"; import { GEO_TYPE_KEYS } from "@/uhm/lib/map/geo/geoTypeMap"; import { clampYearValue } from "@/uhm/lib/utils/timeline"; export type GeometryFocusRequest = { key: number; collection: FeatureCollection; }; type EditorStoreValues = { // Editor mode + baseline FeatureCollection used to seed/reset useEditorState. mode: EditorMode; baselineFeatureCollection: FeatureCollection; // Task flags; setTaskFlag ensures only one blocking task is active at a time. isSaving: boolean; isSubmitting: boolean; isOpeningSection: boolean; // Project/commit state loaded from backend. availableSections: Project[]; selectedProjectId: string; newSectionTitle: string; commitTitle: string; editorUserIdInput: string; activeSection: Project | null; projectState: ProjectState | null; sectionCommits: ProjectCommit[]; baselineSnapshot: EditorSnapshot | null; // Entity state: backend catalog plus snapshot-local rows and form/search status. entityCatalog: Entity[]; snapshotEntityRows: EntitySnapshot[]; entityStatus: string | null; selectedFeatureIds: FeatureId[]; entityForm: EntityFormState; selectedGeometryEntityIds: string[]; geometryMetaForm: GeometryMetaFormState; isEntitySubmitting: boolean; entityFormStatus: string | null; entitySearchResults: Entity[]; isEntitySearchLoading: boolean; // Timeline + background layer state for map filtering/rendering. timelineDraftYear: number; backgroundVisibility: BackgroundLayerVisibility; isBackgroundVisibilityReady: boolean; // Wiki state and entity-wiki relationship snapshot. snapshotWikis: WikiSnapshot[]; snapshotEntityWikiLinks: EntityWikiLinkSnapshot[]; blockedPendingSubmissionId: string | null; // Unified search state shared by entity/wiki/geo search UI. searchKind: UnifiedSearchKind; searchQuery: string; searchQueryDraft: string; wikiSearchResults: Wiki[]; isWikiSearching: boolean; geoSearchResults: EntityGeometriesSearchItem[]; isGeoSearching: boolean; requestedActiveWikiId: string | null; // Layout sizing and panel filter state. leftPanelWidth: number; rightPanelWidth: number; timelineFilterEnabled: boolean; geometryBindingFilterEnabled: boolean; geoBindingStatus: string | null; hoveredGeometryId: string | null; // Map focus/replay visibility state. geometryFocusRequest: GeometryFocusRequest | null; replayFeatureId: string | number | null; hideOutside: boolean; // Map visibility overrides keyed by either a geometry id or a semantic geo type key. geometryVisibility: Record; }; type EditorStoreActions = { setMode: (next: SetStateAction) => void; setBaselineFeatureCollection: (next: SetStateAction) => void; setIsSaving: (next: SetStateAction) => void; setIsSubmitting: (next: SetStateAction) => void; setIsOpeningSection: (next: SetStateAction) => void; setAvailableSections: (next: SetStateAction) => void; setSelectedProjectId: (next: SetStateAction) => void; setNewSectionTitle: (next: SetStateAction) => void; setCommitTitle: (next: SetStateAction) => void; setEditorUserIdInput: (next: SetStateAction) => void; setActiveSection: (next: SetStateAction) => void; setProjectState: (next: SetStateAction) => void; setProjectCommits: (next: SetStateAction) => void; setBaselineSnapshot: (next: SetStateAction) => void; setEntityCatalog: (next: SetStateAction) => void; setSnapshotEntityRows: (next: SetStateAction) => void; setEntityStatus: (next: SetStateAction) => void; setSelectedFeatureIds: (next: SetStateAction) => void; setEntityForm: (next: SetStateAction) => void; setSelectedGeometryEntityIds: (next: SetStateAction) => void; setGeometryMetaForm: (next: SetStateAction) => void; setIsEntitySubmitting: (next: SetStateAction) => void; setEntityFormStatus: (next: SetStateAction) => void; setEntitySearchResults: (next: SetStateAction) => void; setIsEntitySearchLoading: (next: SetStateAction) => void; setTimelineDraftYear: (next: SetStateAction) => void; setBackgroundVisibility: (next: SetStateAction) => void; setIsBackgroundVisibilityReady: (next: SetStateAction) => void; setSnapshotWikis: (next: SetStateAction) => void; setSnapshotEntityWikiLinks: (next: SetStateAction) => void; setBlockedPendingSubmissionId: (next: SetStateAction) => void; setSearchKind: (next: SetStateAction) => void; setSearchQuery: (next: SetStateAction) => void; setSearchQueryDraft: (next: SetStateAction) => void; setWikiSearchResults: (next: SetStateAction) => void; setIsWikiSearching: (next: SetStateAction) => void; setGeoSearchResults: (next: SetStateAction) => void; setIsGeoSearching: (next: SetStateAction) => void; setRequestedActiveWikiId: (next: SetStateAction) => void; setLeftPanelWidth: (next: SetStateAction) => void; setRightPanelWidth: (next: SetStateAction) => void; setTimelineFilterEnabled: (next: SetStateAction) => void; setGeometryBindingFilterEnabled: (next: SetStateAction) => void; setGeoBindingStatus: (next: SetStateAction) => void; setHoveredGeometryId: (next: SetStateAction) => void; setGeometryFocusRequest: (next: SetStateAction) => void; setReplayFeatureId: (next: SetStateAction) => void; setHideOutside: (next: SetStateAction) => void; setGeometryVisibility: (next: SetStateAction>) => void; }; export type EditorStoreState = EditorStoreValues & EditorStoreActions; export type EditorStoreApi = StoreApi; export type EditorStoreOptions = { emptyFeatureCollection: FeatureCollection; defaultEditorUserId: string; fallbackTimelineRange: TimelineRange; currentYear: number; }; // Hỗ trợ setter nhận cả value trực tiếp và updater function giống React setState. function resolveNextState(next: SetStateAction, prev: T): T { return typeof next === "function" ? (next as (prevState: T) => T)(prev) : next; } // Khởi tạo visibility mặc định cho từng geo type trong map. function buildInitialGeometryVisibility() { const next: Record = {}; for (const key of GEO_TYPE_KEYS) { next[key] = true; } return next; } export function createEditorStore(options: EditorStoreOptions): EditorStoreApi { // Năm timeline ban đầu luôn nằm trong fixed range để slider/map nhất quán. const initialTimelineYear = clampYearValue( options.currentYear, options.fallbackTimelineRange.min, options.fallbackTimelineRange.max ); return createStore()((set) => { // Setter generic để tránh lặp boilerplate cho từng field trong store. const setValue = ( key: K, next: SetStateAction ) => { set((state) => { const nextValue = resolveNextState(next, state[key]); if (Object.is(nextValue, state[key])) return state; return { [key]: nextValue, } as Pick; }); }; // Setter riêng cho task flags vì saving/submitting/opening không nên đồng thời true. const setTaskFlag = ( task: "saving" | "submitting" | "opening-project", next: SetStateAction ) => { set((state) => { const currentValue = task === "saving" ? state.isSaving : task === "submitting" ? state.isSubmitting : state.isOpeningSection; const nextValue = resolveNextState(next, currentValue); if (nextValue) { return { isSaving: task === "saving", isSubmitting: task === "submitting", isOpeningSection: task === "opening-project", }; } if (!currentValue) { return {}; } if (task === "saving") return { isSaving: false }; if (task === "submitting") return { isSubmitting: false }; return { isOpeningSection: false }; }); }; return { mode: "idle", baselineFeatureCollection: options.emptyFeatureCollection, isSaving: false, isSubmitting: false, isOpeningSection: false, availableSections: [], selectedProjectId: "", newSectionTitle: "", commitTitle: "", editorUserIdInput: options.defaultEditorUserId, activeSection: null, projectState: null, sectionCommits: [], baselineSnapshot: null, entityCatalog: [], snapshotEntityRows: [], entityStatus: null, selectedFeatureIds: [], entityForm: { name: "", description: "", time_start: "", time_end: "", }, selectedGeometryEntityIds: [], geometryMetaForm: { type_key: "", time_start: "", time_end: "", binding: "", }, isEntitySubmitting: false, entityFormStatus: null, entitySearchResults: [], isEntitySearchLoading: false, timelineDraftYear: initialTimelineYear, backgroundVisibility: { ...HIDDEN_BACKGROUND_LAYER_VISIBILITY }, isBackgroundVisibilityReady: false, snapshotWikis: [], snapshotEntityWikiLinks: [], blockedPendingSubmissionId: null, searchKind: "entity", searchQuery: "", searchQueryDraft: "", wikiSearchResults: [], isWikiSearching: false, geoSearchResults: [], isGeoSearching: false, requestedActiveWikiId: null, leftPanelWidth: 280, rightPanelWidth: 420, timelineFilterEnabled: true, geometryBindingFilterEnabled: true, geoBindingStatus: null, hoveredGeometryId: null, geometryFocusRequest: null, replayFeatureId: null, hideOutside: false, geometryVisibility: buildInitialGeometryVisibility(), setMode: (next) => setValue("mode", next), setBaselineFeatureCollection: (next) => setValue("baselineFeatureCollection", next), setIsSaving: (next) => setTaskFlag("saving", next), setIsSubmitting: (next) => setTaskFlag("submitting", next), setIsOpeningSection: (next) => setTaskFlag("opening-project", next), setAvailableSections: (next) => setValue("availableSections", next), setSelectedProjectId: (next) => setValue("selectedProjectId", next), setNewSectionTitle: (next) => setValue("newSectionTitle", next), setCommitTitle: (next) => setValue("commitTitle", next), setEditorUserIdInput: (next) => setValue("editorUserIdInput", next), setActiveSection: (next) => setValue("activeSection", next), setProjectState: (next) => setValue("projectState", next), setProjectCommits: (next) => setValue("sectionCommits", next), setBaselineSnapshot: (next) => setValue("baselineSnapshot", next), setEntityCatalog: (next) => setValue("entityCatalog", next), setSnapshotEntityRows: (next) => setValue("snapshotEntityRows", next), setEntityStatus: (next) => setValue("entityStatus", next), setSelectedFeatureIds: (next) => setValue("selectedFeatureIds", next), setEntityForm: (next) => setValue("entityForm", next), setSelectedGeometryEntityIds: (next) => setValue("selectedGeometryEntityIds", next), setGeometryMetaForm: (next) => setValue("geometryMetaForm", next), setIsEntitySubmitting: (next) => setValue("isEntitySubmitting", next), setEntityFormStatus: (next) => setValue("entityFormStatus", next), setEntitySearchResults: (next) => setValue("entitySearchResults", next), setIsEntitySearchLoading: (next) => setValue("isEntitySearchLoading", next), setTimelineDraftYear: (next) => setValue("timelineDraftYear", next), setBackgroundVisibility: (next) => setValue("backgroundVisibility", next), setIsBackgroundVisibilityReady: (next) => setValue("isBackgroundVisibilityReady", next), setSnapshotWikis: (next) => setValue("snapshotWikis", next), setSnapshotEntityWikiLinks: (next) => setValue("snapshotEntityWikiLinks", next), setBlockedPendingSubmissionId: (next) => setValue("blockedPendingSubmissionId", next), setSearchKind: (next) => setValue("searchKind", next), setSearchQuery: (next) => setValue("searchQuery", next), setSearchQueryDraft: (next) => setValue("searchQueryDraft", next), setWikiSearchResults: (next) => setValue("wikiSearchResults", next), setIsWikiSearching: (next) => setValue("isWikiSearching", next), setGeoSearchResults: (next) => setValue("geoSearchResults", next), setIsGeoSearching: (next) => setValue("isGeoSearching", next), setRequestedActiveWikiId: (next) => setValue("requestedActiveWikiId", next), setLeftPanelWidth: (next) => setValue("leftPanelWidth", next), setRightPanelWidth: (next) => setValue("rightPanelWidth", next), setTimelineFilterEnabled: (next) => setValue("timelineFilterEnabled", next), setGeometryBindingFilterEnabled: (next) => setValue("geometryBindingFilterEnabled", next), setGeoBindingStatus: (next) => setValue("geoBindingStatus", next), setHoveredGeometryId: (next) => setValue("hoveredGeometryId", next), setGeometryFocusRequest: (next) => setValue("geometryFocusRequest", next), setReplayFeatureId: (next) => setValue("replayFeatureId", next), setHideOutside: (next) => setValue("hideOutside", next), setGeometryVisibility: (next) => setValue("geometryVisibility", next), }; }); } const EditorStoreContext = createContext(null); type EditorStoreProviderProps = { children: ReactNode; options: EditorStoreOptions; }; export function EditorStoreProvider({ children, options }: EditorStoreProviderProps) { // State giữ store instance ổn định trong suốt vòng đời provider. const [store] = useState(() => createEditorStore(options)); return ( {children} ); } export function useEditorStore(selector: (state: EditorStoreState) => T) { // Hook đọc store bằng selector để component chỉ re-render theo slice cần thiết. const store = useContext(EditorStoreContext); if (!store) { throw new Error("useEditorStore must be used within EditorStoreProvider."); } return useStore(store, selector); } export function useEditorStoreApi() { // Hook lấy raw StoreApi cho command layer cần getState/setState ngoài render. const store = useContext(EditorStoreContext); if (!store) { throw new Error("useEditorStoreApi must be used within EditorStoreProvider."); } return store; }