import React, { useEffect, useLayoutEffect, useMemo, useState } from "react";
import { fileSave } from "browser-fs-access"

import { Api as RevealApi } from "reveal.js"

import {
    Box,
    Editor,
    Tldraw,
    DefaultQuickActions,
    DefaultQuickActionsContent,
    TldrawUiMenuItem,
    AlignMenuItems,
    DistributeMenuItems,
    EditLinkMenuItem,
    GroupOrUngroupMenuItem,
    ReorderMenuItems,
    RotateCWMenuItem,
    StackMenuItems,
    DefaultActionsMenu,
    createTLStore,
    defaultShapeUtils,
    throttle,
    PageRecordType,
    TldrawUiMenuSubmenu,
    TldrawUiMenuGroup,
    TLUiActionsContextType,
    useActions,
    DefaultMainMenu,
    EditSubmenu,
    ExportFileContentSubMenu,
    ExtrasGroup,
    PreferencesGroup,
    transact,
    TLShapeId,
    createTLUser,
    TLUserPreferences,
    TldrawUiMenuCheckboxItem,
    TLStore,
    TLStoreSnapshot,
    DefaultToolbar,
    SelectToolbarItem,
    DrawToolbarItem,
    EraserToolbarItem,
    ArrowToolbarItem,
    TextToolbarItem,
    NoteToolbarItem,
    AssetToolbarItem,
    RectangleToolbarItem,
    ArrowDownToolbarItem,
    ArrowLeftToolbarItem,
    ArrowRightToolbarItem,
    ArrowUpToolbarItem,
    CheckBoxToolbarItem,
    CloudToolbarItem,
    DiamondToolbarItem,
    EllipseToolbarItem,
    FrameToolbarItem,
    HexagonToolbarItem,
    HighlightToolbarItem,
    LaserToolbarItem,
    LineToolbarItem,
    OvalToolbarItem,
    RhombusToolbarItem,
    StarToolbarItem,
    TriangleToolbarItem,
    XBoxToolbarItem,
    loadSnapshot,
    getSnapshot,
} from "tldraw"
import { useAtom } from "@tldraw/state-react"

import { makeInt, parseOptionalBoolean } from "./util"
import { defaultStyleProps, getTldrevealConfig } from "./config";

// TODO:
// - store.loadSnapshot is deprecated
// - Somehow create overlaid pages for fragment navigation
// - Fix the overlay in scroll mode
// - Fix the 40 slides with drawings limit (that's tldraw's (artificial) page limit)

const TLDREVEAL_FILE_EXTENSION = ".tldrev"

interface FileSubmenuProps {
    canUseLocalStorage: boolean
    saveToLocalStorage: boolean
}

function FileSubmenu({ canUseLocalStorage, saveToLocalStorage }: FileSubmenuProps) {
    const actions = useActions()

    const mainGroup =
        <TldrawUiMenuGroup  id="tldreveal-file-main">
            <TldrawUiMenuItem {...actions["tldreveal.save-file"]} />
        </TldrawUiMenuGroup>

    if (canUseLocalStorage) {
        return (
            <TldrawUiMenuSubmenu id="tldreveal-file" label="tldreveal.menu.file">
                {mainGroup}
                <TldrawUiMenuGroup id="tldreveal-file-localstorage">
                    <TldrawUiMenuCheckboxItem
                        checked={saveToLocalStorage}
                        {...actions["tldreveal.toggle-save-to-localstorage"]}
                    />
                    <TldrawUiMenuItem {...actions["tldreveal.clear-localstorage"]} />
                </TldrawUiMenuGroup>
            </TldrawUiMenuSubmenu>
        )
    } else {
        return mainGroup
    }
}

function ClearSubmenu() {
    const actions = useActions()
    return (
        <TldrawUiMenuSubmenu id="tldreveal-clear" label="tldreveal.menu.clear">
            <TldrawUiMenuGroup id="tldreveal-clear-group">
                <TldrawUiMenuItem {...actions["tldreveal.clear-page"]} />
                <TldrawUiMenuItem {...actions["tldreveal.clear-deck"]} />
            </TldrawUiMenuGroup>
        </TldrawUiMenuSubmenu>
    )
}

function CustomMainMenu({ fileProps }: { fileProps: FileSubmenuProps }) {
    const actions = useActions()
    return (
        <DefaultMainMenu>
            <FileSubmenu {...fileProps} />
            <ClearSubmenu />
            <EditSubmenu />
            <ExportFileContentSubMenu />
            <ExtrasGroup />
            <PreferencesGroup />
            <TldrawUiMenuGroup id="close">
                <TldrawUiMenuItem {...actions["tldreveal.close"]} />
            </TldrawUiMenuGroup>
        </DefaultMainMenu>
    )
}

function CustomQuickActions() {
    const actions = useActions()
    return (
        <DefaultQuickActions>
            <TldrawUiMenuItem {...actions["tldreveal.close"]} />
            <DefaultQuickActionsContent />
        </DefaultQuickActions>
    )
}

function CustomActionsMenu() {
    return (
        <DefaultActionsMenu>
            <AlignMenuItems />
            <DistributeMenuItems />
            <StackMenuItems />
            <ReorderMenuItems />
            <RotateCWMenuItem />
            <EditLinkMenuItem />
            <GroupOrUngroupMenuItem />
        </DefaultActionsMenu>
    )
}

function CustomToolbar() {
    return (
        <DefaultToolbar>
            <SelectToolbarItem />
            <DrawToolbarItem />
            <EraserToolbarItem />
            <ArrowToolbarItem />
            <TextToolbarItem />
            <NoteToolbarItem />
            <AssetToolbarItem />
            <RectangleToolbarItem />
            <EllipseToolbarItem />
            <TriangleToolbarItem />
            <DiamondToolbarItem />
            <HexagonToolbarItem />
            <OvalToolbarItem />
            <RhombusToolbarItem />
            <StarToolbarItem />
            <CloudToolbarItem />
            <XBoxToolbarItem />
            <CheckBoxToolbarItem />
            <ArrowLeftToolbarItem />
            <ArrowUpToolbarItem />
            <ArrowDownToolbarItem />
            <ArrowRightToolbarItem />
            <LineToolbarItem />
            <HighlightToolbarItem />
            <LaserToolbarItem />
            <FrameToolbarItem />
        </DefaultToolbar>
    )
}

export interface TldrevealOverlayProps {
    /// The instance of Reveal this overlaid on
    reveal: RevealApi
    /// The container element in which the overlay is rendered
    container: HTMLDivElement
}

export function TldrevealOverlay({ reveal, container }: TldrevealOverlayProps) {
    const config = getTldrevealConfig(reveal)

    function tryGetId(element: HTMLElement) : string | undefined {
        return element.getAttribute("data-tlid") || element.id || undefined
    }

    const deckId : string | undefined = useMemo(() => 
        tryGetId(reveal.getSlidesElement()) || tryGetId(reveal.getRevealElement())
    , [])

    const saveToLocalStorageKey = deckId && `TLDREVEAL_SAVE_TO_LOCALSTORAGE__${deckId}`
    const localStorageKey = deckId && `TLDREVEAL_SNAPSHOT__${deckId}`

    const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
    const [editor, setEditor] = useState<Editor | undefined>()

    // Use a local user preferences atom, to prevent sharing dark mode status
    // across multiple instances
    const userPreferences = useAtom<TLUserPreferences>("userPreferences", { id: "tldreveal", colorScheme: config.isDarkMode ? "dark" : "light" })
    const [isolatedUser] = useState(() => createTLUser({ userPreferences, setUserPreferences: userPreferences.set }))

    const [saveToLocalStorage, setSaveToLocalStorage_] = 
        useState(parseOptionalBoolean(localStorage.getItem(saveToLocalStorageKey)) ?? config.useLocalStorage)
    
    function setSaveToLocalStorage(value: boolean) {
        setSaveToLocalStorage_(value)
        localStorage.setItem(saveToLocalStorageKey, value ? "true" : "false")
    }
    
    const [isReady, setIsReady] = useState(false)
    const [isShown, setIsShown] = useState(true)
    const [isEditing, setIsEditing] = useState(false)

    const [currentSlide, setCurrentSlide] = useState<{ h: number, v: number }>(reveal.getIndices())

    const slideWidth = makeInt(reveal.getConfig().width)
    const slideHeight = makeInt(reveal.getConfig().height)
    const bounds = new Box(0, 0, slideWidth, slideHeight)

    function getSlideId(index: { h: number, v: number }) : string {
        const slideElement = reveal.getSlide(index.h, index.v)

        const givenId = tryGetId(slideElement)
        if (givenId !== undefined) {
            return givenId
        }

        if (index.v !== 0) {
            const stackId = tryGetId(slideElement.parentElement)
            if (stackId !== undefined) {
                return `${stackId}.${index.v}`
            }

            const firstInStackId = getSlideId({ h: index.h, v: 0 })
            if (firstInStackId.match("^\\d+\\.0$")) {
                return firstInStackId.replace(".0", `.${index.v}`)
            } else {
                return `${firstInStackId}.${index.v}`
            }
        }

        return `${index.h}.${index.v}`
    }

    const currentSlideId = useMemo(() => getSlideId(currentSlide), [ currentSlide ])

    function getTimestampedSnapshot(store: TLStore) : TLStoreSnapshot & { timestamp: number } {
        const { document } = getSnapshot(store)
        return {
            timestamp: Date.now(),
            ...document
        }
    }

    async function loadInitial(store: TLStore) {
        let localStorageSnapshot: (TLStoreSnapshot & { timestamp: number }) | undefined
        if (localStorageKey) {
            const snapshotJson = localStorage.getItem(localStorageKey)
            if (snapshotJson) {
                localStorageSnapshot = JSON.parse(snapshotJson)
            }
        }

        let uriSnapshot: (TLStoreSnapshot & { timestamp: number }) | undefined
        if (config.snapshotUrl) {
            let uri
            if (config.snapshotUrl === "auto") {
                const path = window.location.pathname
                const ext = [ ".html", ".htm" ].find(ext => path.endsWith(ext))
                if (ext) {
                    uri = path.substring(0, path.length - ext.length) + ".tldrev"
                } else {
                    uri = "index.tldrev"
                }
            } else {
                uri = config.snapshotUrl.url
            }

            try {
                const res = await fetch(uri)
                if (res.ok) {
                    const snapshotJson = await res.text()
                    try {
                        const snapshot = JSON.parse(snapshotJson)
                        if (!(snapshot.timestamp && snapshot.store && snapshot.schema)) {
                            console.warn("Received invalid snapshot from", uri)
                        } else {
                            uriSnapshot = snapshot
                        }
                    } catch {
                        console.warn("Received invalid snapshot from", uri)
                    }
                } else {
                    if (config.snapshotUrl === "auto") {
                        console.log("No saved drawings found at auto-detected uri:", uri, "Got status:", res.status, res.statusText)
                    } else {
                        console.warn("Failed to load saved drawings from", uri, "Got status:", res.status, res.statusText)
                    }
                }
            } catch (err) {
                console.warn("Failed to fetch drawings from uri. Error:", err)
            }
        }

        let snapshot: (TLStoreSnapshot & { timestamp: number }) | undefined
        if (localStorageSnapshot && uriSnapshot) {
            if (localStorageSnapshot.timestamp >= uriSnapshot.timestamp) {
                snapshot = localStorageSnapshot
            } else {
                // TODO: dialog, for now always load newest
                snapshot = uriSnapshot
            }
        } else {
            snapshot = localStorageSnapshot || uriSnapshot
        }

        if (snapshot) {
            loadSnapshot(store, snapshot)
        }

        setIsReady(true)
    }

    // https://tldraw.dev/examples/data/assets/local-storage
    useLayoutEffect(() => {
        loadInitial(store)
    }, [ store ])

    useEffect(() => {
        // If we can and want to use local storage, then listen to changes of
        // the store and save them
        if (localStorageKey && saveToLocalStorage) {
            const cleanupStoreListen = store.listen(
                throttle(() => {
                    const snapshot = getTimestampedSnapshot(store)
                    localStorage.setItem(localStorageKey, JSON.stringify(snapshot))
                }, 500)
            )

            return () => {
                cleanupStoreListen()
            }
        }
    }, [ store, saveToLocalStorage ])

    function initializeEditor(state = { editor, currentSlide }) {
        state.editor.setCurrentTool("draw")
        for (const style of Object.keys(config.defaultStyles)) {
            if (config.defaultStyles[style]) {
                state.editor.setStyleForNextShapes(defaultStyleProps[style], config.defaultStyles[style])
            }
        }
        state.editor.updateInstanceState({ 
            isDebugMode: false,
            exportBackground: false
        })
        // Lock the camera
        state.editor.setCameraOptions({
            // Not necessary, but the wheel shouldn't move the camera
            wheelBehavior: "none",
            // There are no zoom steps available to the user
            zoomSteps: [],
            constraints: {
                baseZoom: "fit-max",
                behavior: "fixed",
                bounds: bounds,
                initialZoom: "fit-max",
                origin: { x: 0, y: 0 },
                padding: { x: 0, y: 0 }
            }
        })
        syncEditor(state)
    }

    function onTldrawMount(editor: Editor) {
        setEditor(editor)
        initializeEditor({ editor, currentSlide })
    }

    useEffect(() => {
        container.classList.toggle("tldreveal-hidden", !isShown)

        if (isShown && isEditing) {
            container.classList.remove("tldreveal-inactive")
            container.setAttribute("data-prevent-swipe", "true")
        } else {
            if (!container.classList.contains("tldreveal-inactive")) {
                container.classList.add("tldreveal-inactive")
            }
            if (container.hasAttribute("data-prevent-swipe")) {
                container.removeAttribute("data-prevent-swipe")
            }
        }
    }, [ isShown, isEditing ])

    function handleDKey() {
        setIsShown(true)
        setIsEditing(true)
    }

    function handleVKey() {
        setIsEditing(false)
        setIsShown(isShown => !isShown)
    }

    const handleKeydown = (state = { isEditing }) => (event: KeyboardEvent) => {
        if (state.isEditing && event.key === "Escape") {
            setIsEditing(false)
            event.stopImmediatePropagation()
        }
    }

    const handleDblclick = (state = { isEditing }) => (event: MouseEvent) => {
        if (!state.isEditing) {
            setIsShown(true)
            setIsEditing(true)
            event.stopImmediatePropagation()
        }
    }

    useEffect(() => {
        const state = { isEditing }
        const handleKeydown_ = handleKeydown(state)
        const handleDblclick_ = handleDblclick(state)

        reveal.addKeyBinding({ keyCode: 68, key: "D", description: "Enter drawing mode" }, handleDKey)
        reveal.addKeyBinding({ keyCode: 86, key: "V", description: "Toggle drawing visibility" }, handleVKey)
        window.addEventListener("dblclick", handleDblclick_, true)
        window.addEventListener("keydown", handleKeydown_, true)
        return () => {
            reveal.removeKeyBinding(68)
            window.removeEventListener("dblclick", handleDblclick_, true)
            window.removeEventListener("keydown", handleKeydown_, true)
        }
    }, [ isEditing ])

    function handleReady(event) {
        setCurrentSlide({ h: event.indexh, v: event.indexv })
    }
    
    const handleSlidechanged = (state = { currentSlideId }) => event => {
        const currentTransition: string | undefined = 
            reveal.getConfig().transition 
            || event.currentSlide.getAttribute("data-transition")
        const noTransition = currentTransition === "none" || currentTransition?.includes("none-in")
        const hasSameSlideId = getSlideId({ h: event.indexh, v: event.indexv }) === state.currentSlideId
        if (noTransition || hasSameSlideId) {
            setCurrentSlide({ h: event.indexh, v: event.indexv })
        } else {
            container.classList.toggle("start-transition", true)
            setTimeout(() => {
                setCurrentSlide({ h: event.indexh, v: event.indexv })
                container.classList.toggle("transitioning", true)
            }, 200)
        }
    }
    
    function handleSlidetransitionend(_event) {
        container.classList.toggle("start-transition", false)
        container.classList.toggle("transitioning", false)
    }
    
    function handleOverviewshown(_event) {
        setIsShown(false)
    }
    
    function handleOverviewhidden(_event) {
        setIsShown(true)
    }

    function handlePaused(_event) {
        setIsShown(false)
    }

    function handleResumed(_event) {
        setIsShown(true)
    }
    
    useEffect(() => {
        reveal.on("ready", handleReady)
        // beforeslidechange
        reveal.on("slidetransitionend", handleSlidetransitionend)
        reveal.on("overviewshown", handleOverviewshown)
        reveal.on("overviewhidden", handleOverviewhidden)
        reveal.on("paused", handlePaused)
        reveal.on("resumed", handleResumed)
        return () => {
            reveal.off("ready", handleReady)
            reveal.off("overviewshown", handleOverviewshown)
            reveal.off("overviewhidden", handleOverviewhidden)
            reveal.off("paused", handlePaused)
            reveal.off("resumed", handleResumed)
        }
    }, [])
    
    useEffect(() => {
        const handleSlidechanged_ = handleSlidechanged({ currentSlideId })
        reveal.on("slidechanged", handleSlidechanged_)
        return () => {
            reveal.off("slidechanged", handleSlidechanged_)
        }
    }, [ currentSlideId ])

    const handleResize = (state = { editor }) => {
        return () => resetCamera(state)
    }

    useEffect(() => {
        const state = { editor }
        const handleResize_ = handleResize(state)

        window.addEventListener("resize", handleResize_)
        return () => {
            window.removeEventListener("resize", handleResize_)
        }
    }, [ editor ])

    function resetCamera(state = { editor }) {
        if (state.editor) {
            state.editor.setCamera(state.editor.getCamera(), { reset: true })
        }
    }

    function syncEditor(state = { editor, currentSlide }) {
        if (state.editor) {
            // Find the correct page, or create it if there isn't one
            const pageId = PageRecordType.createId(currentSlideId)
            if (!state.editor.getPage(pageId)) {
                state.editor.createPage({ id: pageId, name: currentSlideId })
            }

            // Navigate to the correct page if we're not there yet, and delete
            // the previous page if it is empty
            const oldCurrentPageId = state.editor.getCurrentPageId()
            if (oldCurrentPageId !== pageId) {
                // Delete the old current page if it has no shapes on it
                const deleteOldCurrent = 
                    state.editor.getCurrentPageShapeIds().size === 0

                state.editor.setCurrentPage(pageId)
                // Reset undo/redo to prevent undoing changes on other pages
                state.editor.clearHistory()

                if (deleteOldCurrent) {
                    state.editor.deletePage(oldCurrentPageId)
                }
            }

            if (config.automaticDarkMode) {
                const currentSlideClasses = reveal.getSlide(currentSlide.h, currentSlide.v).classList
                const currentVSlideClasses = reveal.getSlide(currentSlide.h).classList
                if (currentSlideClasses.contains("has-dark-background")) {
                    userPreferences.update(u => ({ ...u, colorScheme: "dark" }))
                } else if (currentSlideClasses.contains("has-light-background")) {
                    userPreferences.update(u => ({ ...u, colorScheme: "light" }))
                } else if (currentVSlideClasses.contains("has-dark-background")) {
                    userPreferences.update(u => ({ ...u, colorScheme: "dark" }))
                } else if (currentVSlideClasses.contains("has-light-background")) {
                    userPreferences.update(u => ({ ...u, colorScheme: "light" }))
                } else {
                    userPreferences.update(u => ({ ...u, colorScheme: config.isDarkMode ? "dark" : "light" }))
                }
            }

            // Reset the camera on each new page
            resetCamera(state)
        }
    }

    useEffect(() => {
        syncEditor({ editor, currentSlide })
    }, [ editor, currentSlide ])

    const customTranslations = {
        en: {
            "tldreveal.menu.file": "File",
            "tldreveal.menu.clear": "Clear",
            "tldreveal.action.close": "Exit drawing mode",
            "tldreveal.action.save-file": "Save file",
            "tldreveal.action.clear-localstorage": "Clear browser storage",
            "tldreveal.action.clear-page": "Clear current slide",
            "tldreveal.action.clear-deck": "Clear deck",

            "tldreveal.options.save-to-localstorage": "Save in browser",

            // Set the default name for built-in export functions
            "document.default-name": deckId || "unknown"
        }
    }

    const customActions : TLUiActionsContextType = {
        ["tldreveal.close"]: {
            id: "tldreveal.close",
            label: "tldreveal.action.close",
            // Available icon types: https://github.com/tldraw/tldraw/blob/main/packages/tldraw/src/lib/ui/icon-types.ts
            icon: "cross-2",
            readonlyOk: true,
            async onSelect(_source) {
                setIsEditing(false)
            }
        },
        ["tldreveal.save-file"]: {
            id: "tldreveal.save-file",
            label: "tldreveal.action.save-file",
            readonlyOk: true,
            kbd: "$s",
            async onSelect(_source) {
                const snapshot = getTimestampedSnapshot(store)
                await fileSave(new Blob([ JSON.stringify(snapshot) ]), {
                    fileName: (deckId || "untitled") + TLDREVEAL_FILE_EXTENSION, 
                    extensions: [ TLDREVEAL_FILE_EXTENSION ]
                })
            }
        },
        ["tldreveal.toggle-save-to-localstorage"]: {
            id: "tldreveal.toggle-save-to-localstorage",
            label: "tldreveal.options.save-to-localstorage",
            readonlyOk: true,
            checkbox: true,
            async onSelect(_source) {
                setSaveToLocalStorage(localStorageKey && !saveToLocalStorage)
            }
        },
        ["tldreveal.clear-localstorage"]: {
            id: "tldreveal.clear-localstorage",
            label: "tldreveal.action.clear-localstorage",
            async onSelect(_source) {
                if (localStorageKey) {
                    setSaveToLocalStorage(false)
                    localStorage.removeItem(localStorageKey)
                }
            }
        },
        ["tldreveal.clear-page"]: {
            id: "tldreveal.clear-page",
            label: "tldreveal.action.clear-page",
            async onSelect(_source) {
                transact(() => {
                    // Delete all shapes on the current page
                    editor.deleteShapes(editor.getCurrentPageShapes())
                })
            }
        },
        ["tldreveal.clear-deck"]: {
            id: "tldreveal.clear-deck",
            label: "tldreveal.action.clear-deck",
            async onSelect(_source) {
                transact(() => {
                    // Find all shapes from the store and delete them
                    const allShapeIds = 
                        store.allRecords()
                            .filter(record => record.typeName === "shape")
                            .map(record => record.id as TLShapeId)
                    editor.deleteShapes(allShapeIds)

                    // Delete all assets
                    editor.deleteAssets(editor.getAssets())

                    // Delete all pages except the current
                    const currentPage = editor.getCurrentPage()
                    for (const page of editor.getPages()) {
                        if (page.id !== currentPage.id) editor.deletePage(page)
                    }
                })
            }
        }
    }

    if (isReady) {
        return (
            <Tldraw
                forceMobile
                hideUi={!isEditing}
                store={store}
                user={isolatedUser}
                onMount={onTldrawMount}
                components={{
                    PageMenu: null,
                    MainMenu: () => CustomMainMenu({ 
                        fileProps: { 
                            canUseLocalStorage: localStorageKey !== undefined, 
                            saveToLocalStorage
                        }
                    }),
                    ActionsMenu: CustomActionsMenu,
                    QuickActions: CustomQuickActions,
                    Toolbar: CustomToolbar
                }}
                overrides={{
                    translations: customTranslations,
                    tools(editor, tools) {
                        // Remove the keyboard shortcut for the hand tool
                        tools.hand.kbd = undefined
                        return tools
                    },
                    // Remove actions related to zooming
                    actions(editor, actions) {
                        delete actions["select-zoom-tool"]
                        delete actions["zoom-in"]
                        delete actions["zoom-out"]
                        delete actions["zoom-to-100"]
                        delete actions["zoom-to-fit"]
                        delete actions["zoom-to-selection"]
                        delete actions["back-to-content"]
                        return { ...actions, ...customActions }
                    }
                }}
                >
            </Tldraw>
        )
    }
}
