import { safeDestr as parse } from 'destr';
import { Canvas } from "#shared/canvas.util";
import render from "#shared/markdown.util";
import { confirm, contextmenu, popper } from "#shared/floating.util";
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
import prose, { h1, h2, loading } from "#shared/proses";
import { getID, ID_SIZE, parsePath } from '#shared/general.util';
import { TreeDOM, type Recursive } from '#shared/tree';
import { History } from '#shared/history.util';
import { MarkdownEditor } from '#shared/editor.util';
import type { CanvasContent } from '~/types/canvas';

import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
import { draggable, dropTargetForElements, monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
import { type Instruction, attachInstruction, extractInstruction } from '@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item';
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
import type { CleanupFn } from '@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types';

export type FileType = keyof ContentMap;
export interface ContentMap
{
    markdown: string;
    file: string;
    canvas: CanvasContent;
    map: string;
    folder: null;
}
export interface Overview<T extends FileType>
{
    id: string;
    path: string;
    owner: number;
    title: string;
    timestamp: Date;
    navigable: boolean;
    private: boolean;
    order: number;
    type: T;
}
export type ProjectContent<T extends FileType = FileType> = Overview<T> & { content: ContentMap[T] };

class AsyncQueue
{
    private size: number;
    private count: number = 0;
    private _queue: Array<() => Promise<any>>;

    promise: Promise<void> = Promise.resolve();
    finished: boolean = true;

    private res: (value: void | PromiseLike<void>) => void = () => {};
    private rej: (value: void | PromiseLike<void>) => void = () => {};

    constructor(size: number = 8)
    {
        this.size = size;
        this._queue = [];
    }

    queue(fn: () => Promise<any>): Promise<void>
    {
        if(this.finished)
        {
            this.finished = false;
    
            this.promise = new Promise((res, rej)  => { 
                this.res = res;
                this.rej = rej;
            });
        }

        this._queue.push(fn);
        this.refresh();

        return this.promise;
    }

    private refresh()
    {
        for(let i = this.count; i < this.size && this._queue.length > 0; i++)
        {
            this.count++;

            const fn = this._queue.shift()!;
            fn().catch(e => this.rej(e)).then(() => {
                this.count--;
                this.refresh();
            });
        }

        if(this.count === 0 && this._queue.length === 0 && !this.finished)
        {
            this.finished = true;
            this.res();
        }
    }
}

export const DEFAULT_CONTENT: Record<FileType, ContentMap[FileType]> = {
    map: {},
    canvas: { nodes: [], edges: []},
    markdown: '',
    file: '',
    folder: null,
};
export type LocalContent<T extends FileType = FileType> = ProjectContent<T> & { 
    localEdit?: boolean;
    error?: boolean;
};
export class Content
{
    private static _ready = false;
    private static initPromise?: Promise<boolean>;

    private static root: FileSystemDirectoryHandle;

    private static _overview: Record<string, Omit<LocalContent, 'content'>>;
    private static queue = new AsyncQueue();

    static init(): Promise<boolean>
    {
        if(Content._ready)
            return Promise.resolve(true);

        Content.initPromise = new Promise(async (res) => {
            try
            {
                if(!('storage' in navigator))
                    return false;
        
                Content.root = await navigator.storage.getDirectory();
    
                const overview = await Content.read('overview', { create: true });
                try
                {
                    Content._overview = parse<Record<string, Omit<LocalContent, 'content'>>>(overview);
                }
                catch(e)
                {
                    Content._overview = {};
                    await Content.pull();
                }
    
                Content._ready = true;
            }
            catch(e)
            {
                console.error(e);
            }
    
            res(Content._ready);
        });

        return Content.initPromise;
    }
    static getFromPath(path: string, content: boolean = false)
    {
        return Object.values(Content._overview).find(e => e.path === path);
    }
    static get(id: string, content: boolean = false)
    {
        return Content._overview[id];
    }
    static async getContent(id: string): Promise<LocalContent | undefined>
    {
        const overview = Content._overview[id];

        if(!overview)
            return;

        return { ...overview, content: Content.fromString(overview, (await Content.read(id, { create: true }))!) };
    }
    static set(id: string, overview?: Omit<LocalContent, 'content'> | Recursive<Omit<LocalContent, 'content'>>)
    {
        if(overview === undefined)
        {
            delete Content._overview[id];
        }
        else
        {
            const { 
                id: _id,
                path: _path,
                owner: _owner,
                title: _title,
                timestamp: _timestamp,
                navigable: _navigable,
                private: _private,
                order: _order,
                type: _type,
                ...rest
            } = overview as Recursive<LocalContent>;
            Content._overview[id] = {
                id: _id,
                path: _path,
                owner: _owner,
                title: _title,
                timestamp: _timestamp,
                navigable: _navigable,
                private: _private,
                order: _order,
                type: _type,
            };
        }
    }
    static async save(content?: ProjectContent)
    {
        const overviewAsString = JSON.stringify(Content._overview)
        Content.queue.queue(() => Content.write("overview", overviewAsString));

        if(content && content.content)
        {
            const contentAsString = Content.toString(content);
            Content.queue.queue(() => Content.write(content.id, contentAsString));
        }

        return Content.queue.promise;
    }
    static async pull()
    {
        const overview = (await useRequestFetch()('/api/file/overview', { cache: 'no-cache' })) as ProjectContent<FileType>[] | undefined;

        if(!overview)
        {
            //TODO: Cannot get data :'(
            //Add a warning ?
            return;
        }

        for(const file of overview)
        {
            const _overview = Content._overview[file.id];
            if(_overview && _overview.localEdit)
            {
                //TODO: Ask what to do about this file.
            }
            else
            {
                Content._overview[file.id] = file;

                if(file.type === 'folder')
                    continue;

                Content.queue.queue(() => {
                    return useRequestFetch()(`/api/file/content/${file.id}`, { cache: 'no-cache' }).then(async (content: string | undefined) => {
                        if(content)
                            Content.queue.queue(() => Content.write(file.id, content, { create: true }));
                        else
                            Content._overview[file.id].error = true;
                    }).catch(e => {
                        Content._overview[file.id].error = true;
                    });
                });
            }
        }

        return Content.queue.queue(() => {
            return Content.write('overview', JSON.stringify(Content._overview), { create: true });
        });
    }
    static async push()
    {
        const blocked = (await useRequestFetch()('/api/file/overview', { method: 'POST', body: Object.values(Content._overview), cache: 'no-cache' }));

        for(const [id, value] of Object.entries(Content._overview).filter(e => !blocked.includes(e[0])))
        {
            if(value.type === 'folder')
                continue;
            
            Content.queue.queue(() => Content.read(id).then(e => {
                if(e) Content.queue.queue(() => useRequestFetch()(`/api/file/content/${id}`, { method: 'POST', body: e, cache: 'no-cache' }));
            }));
        }

        return Content.queue.promise;
    }
    //Maybe store the file handles ? Is it safe to keep them ? 
    private static async read(path: string, options?: FileSystemGetFileOptions): Promise<string | undefined>
    {
        try
        {
            console.time(`Reading '${path}'`);
            const handle = await Content.root.getFileHandle(path, options);
            const file = await handle.getFile();
    
            const text = await file.text();
            console.timeEnd(`Reading '${path}'`);
            return text;
        }
        catch(e)
        {
            console.error(path, e);
            console.timeEnd(`Reading '${path}'`);
        }
    }
    //Easy to use, but not very performant.
    private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void>
    {
        const size = new TextEncoder().encode(content).byteLength;
        console.time(`Writing ${size} bytes to '${path}'`);
        try
        { 
            const handle = await Content.root.getFileHandle(path, options);
            const file = await handle.createWritable({ keepExistingData: false });

            await file.write(content);
            await file.close();
        }
        catch(e)
        {
            console.error(path, e);
        }
        console.timeEnd(`Writing ${size} bytes to '${path}'`);
    }

    static get estimate(): Promise<StorageEstimate>
    {
        return Content._ready ? navigator.storage.estimate() : Promise.reject();
    }

    static toString<T extends FileType>(content: ProjectContent<T>): string
    {
        return handlers[content.type].toString(content.content);
    }
    
    static fromString<T extends FileType>(overview: Omit<ProjectContent<T>, 'content'>, content: string): ContentMap[T]
    {
        return handlers[overview.type].fromString(content);
    }

    static render(parent: HTMLElement, path: string): Omit<LocalContent, 'content'> | undefined
    {
        const overview = Content.getFromPath(path);

        if(!!overview)
        {
            const load = dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]);
            parent.appendChild(load);

            function _render<T extends FileType>(content: LocalContent<T>): void
            {
                const el = handlers[content.type].render(content);
                el && parent.replaceChild(el, load);
            }

            Content.getContent(overview.id).then(content => _render(content!));
        }
        else
        {
            parent.appendChild(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" }));
        }

        return overview;
    }

    static get files()
    {
        return Object.freeze(Content._overview);
    }
    static get tree()
    {
        const arr: Recursive<Omit<LocalContent, 'content'>>[] = [];

        function addChild(arr: Recursive<Omit<LocalContent, 'content'>>[], overview: Omit<LocalContent, 'content'>): void {
            const parent = arr.find(f => overview.path.startsWith(f.path));
            
            if(parent)
            {
                if(!parent.children)
                    parent.children = [];
                
                (overview as Recursive<typeof overview>).parent = parent;
                addChild(parent.children, overview);
            }
            else
            {
                arr.push({ ...overview });
                arr.sort((a, b) => {
                    if(a.order !== b.order)
                        return a.order - b.order;
                    return a.title.localeCompare(b.title);
                });
            }
        }

        for(const element of Object.values(Content._overview))
        {
            addChild(arr, {...element});
        }

        return arr;
    }
    static get ready(): Promise<boolean>
    {
        return Content._ready ? Promise.resolve(true) : Content.initPromise ?? Promise.resolve(false);
    }
}

type ContentTypeHandler<T extends FileType> = {
    toString: (content: ContentMap[T]) => string;
    fromString: (str: string) => ContentMap[T];
    render: (content: LocalContent<T>) => Node;
    renderEditor: (content: LocalContent<T>) => Node;
};

const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
    canvas: {
        toString: (content) => JSON.stringify(content),
        fromString: (str) => JSON.parse(str),
        render: (content) => new Canvas(content.content).container,
        renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
    },
    markdown: {
        toString: (content) => content,
        fromString: (str) => str,
        render: (content) => {
            return dom('div', { class: 'flex flex-1 justify-start items-start flex-col lg:px-16 xl:px-32 2xl:px-64 py-6' }, [
                dom('div', { class: 'flex flex-1 flex-row justify-between items-center' }, [
                    prose('h1', h1, [text(content.title)]),
                    dom('div', { class: 'flex gap-4' }, [
                        //TODO: Edition link
                    ]),
                ]),
                render(content.content),
            ])
        },
        renderEditor: (content) => {
            let element: HTMLElement;
            if(content.hasOwnProperty('content'))
            {
                MarkdownEditor.singleton.content = content.content;
                element = MarkdownEditor.singleton.dom;
            }
            else
            {
                element = loading("large");
                Content.getContent(content.id).then(e => {
                    if(!e)
                        return element.parentElement?.replaceChild(dom('div', { class: '', text: 'Une erreur est survenue.' }), element);

                    MarkdownEditor.singleton.content = content.content = (e as typeof content).content;
                    element.parentElement?.replaceChild(MarkdownEditor.singleton.dom, element);
                });
            }
            MarkdownEditor.singleton.onChange = (value) => { content.content = value; };
            return dom('div', { class: 'flex flex-1 justify-start items-start flex-col lg:px-16 xl:px-32 2xl:px-64 py-6' }, [
                dom('div', { class: 'flex flex-row justify-between items-center' }, [ prose('h1', h1, [text(content.title)]) ]),
                dom('div', { class: 'flex flex-1 w-full justify-stretch items-stretch py-2 px-1.5 font-sans text-base' }, [element]),
            ])
        },
    },
    file: {
        toString: (content) => content,
        fromString: (str) => str,
        render: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
        renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
    },
    map: {
        toString: (content) => content,
        fromString: (str) => str,
        render: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
        renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
    },
    folder: {
        toString: (_) => '',
        fromString: (_) => null,
        render: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
        renderEditor: (content) => prose('h2', h2, [text('En cours de développement')], { class: 'flex-1 text-center' }),
    }
};

export const iconByType: Record<FileType, string> = {
    'folder': 'lucide:folder',
    'canvas': 'ph:graph-light',
    'file': 'radix-icons:file',
    'markdown': 'radix-icons:file-text',
    'map': 'lucide:map',
};

export class Editor
{
    tree: TreeDOM;
    container: HTMLDivElement;

    selected?: Recursive<LocalContent & { element?: HTMLElement }>;

    private instruction: HTMLDivElement;
    private cleanup: CleanupFn;

    private history: History;

    constructor()
    {
        this.history = new History();
        this.history.register('overview', {
            move: {
                undo: (action) => {
                    this.tree.tree.remove(action.element.id);
                    action.element.parent = action.from.parent;
                    action.element.order = action.from.order;
                    const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
                    
                    this.tree.tree.insertAt(action.element, action.to.order as number);
                    action.element.element = this.tree.render(action.element, depth);
                    action.element?.cleanup();
                    this.dragndrop(action.element, depth, action.element.parent);
                    this.tree.update();

                    action.element.path = path;
                },
                redo: (action) => {
                    this.tree.tree.remove(action.element.id);
                    action.element.parent = action.to.parent;
                    action.element.order = action.to.order;
                    const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
                    
                    this.tree.tree.insertAt(action.element, action.to.order as number);

                    action.element.element = this.tree.render(action.element, depth);
                    action.element?.cleanup();
                    this.dragndrop(action.element, depth, action.element.parent);
                    this.tree.update();

                    action.element.path = path;
                },
            },
            add: {
                undo: (action) => {
                    this.tree.tree.remove(action.element.id);
                    if(this.selected === action.element) this.select();
                    action.element.cleanup();
                    action.element.element?.remove();

                },
                redo: (action) => {
                    if(!action.element)
                    {
                        const depth = getPath(action.element as LocalContent).split('/').length;
                        action.element.element = this.tree.render(action.element as LocalContent, depth) as HTMLElement;
                        this.dragndrop(action.element as LocalContent, depth, (action.element as Recursive<LocalContent>).parent);
                    }
                    this.tree.tree.insertAt(action.element as Recursive<LocalContent>, action.to as number);
                    this.tree.update();
                },
            },
            remove: {
                undo: (action) => {
                    this.tree.tree.insertAt(action.element, action.from as number);
                    this.tree.update();
                },
                redo: (action) => {
                    this.tree.tree.remove(action.element.id);
                    if(this.selected === action.element) this.select();
                    action.element.cleanup();
                    action.element.element?.remove();
                },
            },
            rename: {
                undo: (action) => {
                    action.element.title = action.from;
                    action.element.element!.children[0].children[1].textContent = action.from;
                    action.element.element!.children[0].children[1].setAttribute('title', action.from);

                    const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
                    action.element?.cleanup();
                    this.dragndrop(action.element, depth, action.element.parent);
                    action.element.path = path;
                },
                redo: (action) => {
                    action.element.title = action.to;
                    action.element.element!.children[0].children[1].textContent = action.to;
                    action.element.element!.children[0].children[1].setAttribute('title', action.to);

                    const path = getPath(action.element), depth = path.split("/").filter(e => !!e).length;
                    action.element?.cleanup();
                    this.dragndrop(action.element, depth, action.element.parent);
                    action.element.path = path;
                },
            },
            navigable: {
                undo: (action) => {
                    action.element.navigable = action.from;
                    action.element.element!.children[0].children[2].children[0].replaceWith(icon(action.element.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !action.element.navigable }] }));
                },
                redo: (action) => {
                    action.element.navigable = action.to;
                    action.element.element!.children[0].children[2].children[0].replaceWith(icon(action.element.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !action.element.navigable }] }));
                },
            },
            private: {
                undo: (action) => {
                    action.element.private = action.from;
                    action.element.element!.children[0].children[3].children[0].replaceWith(icon(action.element.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !action.element.private }] }));
                },
                redo: (action) => {
                    action.element.private = action.to;
                    action.element.element!.children[0].children[3].children[0].replaceWith(icon(action.element.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !action.element.private }] }));
                },
            },
        }, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });

        this.tree = new TreeDOM((item, depth) => {
            return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
                icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
                dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
                popper(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), { delay: 150, offset: 8, placement: 'left', arrow: true, content: [text('Navigable')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }),
                popper(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) }  }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }),
            ])]);
        }, (item, depth) => {
            return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
                icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
                dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
                popper(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), { delay: 150, offset: 8, placement: 'left', arrow: true, content: [text('Navigable')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }),
                popper(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) }  }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), { delay: 150, offset: 8, placement: 'right', arrow: true, content: [text('Privé')], class: 'TooltipContent border border-light-30 dark:border-dark-30 px-2 py-1 bg-light-10 dark:bg-dark-10 text-light-70 dark:text-dark-70 z-50' }),
            ])]);
        });

        this.instruction = dom('div', { class: 'absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50' });

        this.cleanup = this.setupDnD();

        this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
    }
    private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
    {
        e.preventDefault();

        const close = contextmenu(e.clientX, e.clientY, [
            dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.add("markdown", item); close() }} }, [icon('radix-icons:plus'), text('Ajouter')]),
            dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-100 dark:text-dark-100', listeners: { click: (e) => { this.rename(item); close() }} }, [icon('radix-icons:input'), text('Renommer')]),
            dom('div', { class: 'hover:bg-light-35 dark:hover:bg-dark-35 px-2 gap-2 flex py-1 items-center cursor-pointer text-light-red dark:text-dark-red', listeners: { click: (e) => { close(); confirm(`Confirmer la suppression de ${item.title}${item.children ? ' et de ses enfants' : ''} ?`).then(e => { if(e) this.remove(item)}) }} }, [icon('radix-icons:trash'), text('Supprimer')]),
        ], { placement: 'right-start', offset: 8 });
    }
    private add(type: FileType, nextTo: Recursive<LocalContent>)
    {
        const count = Object.values(Content.files).filter(e => e.title.match(/^Nouveau( \(\d+\))?$/)).length;
        const item: Recursive<Omit<LocalContent, 'path' | 'content'> & { element?: HTMLElement }> = { id: getID(ID_SIZE), navigable: true, private: false, owner: 0, order: nextTo.order + 1, timestamp: new Date(), title: count === 0 ? 'Nouveau' : `Nouveau (${count})`, type: type, parent: nextTo.parent };
        this.history.add('overview', 'add', [{ element: item, from: undefined, to: nextTo.order + 1 }]);
    }
    private remove(item: LocalContent & { element?: HTMLElement })
    {
        this.history.add('overview', 'remove', [{ element: item, from: item.order, to: undefined }], true);
    }
    private rename(item: LocalContent & { element?: HTMLElement })
    {
        let exists = true;
        const change = () =>
        {
            const value = input.value || item.title;

            if(exists)
            {
                exists = false;

                input.parentElement?.replaceChild(text, input);
                input.remove();
                if(value !== item.title) this.history.add('overview', 'rename', [{ element: item, from: item.title, to: value }], true);
            }
        }
        const text = item.element!.children[0].children[1];
        const input = dom('input', { attributes: { type: 'text', value: item.title }, class: 'bg-light-20 dark:bg-dark-20 outline outline-light-35 dark:outline-dark-35 outline-offset-0 pl-1.5 py-1.5 flex-1', listeners: { mousedown: cancelPropagation, click: cancelPropagation, blur: change, change: change } });

        text.parentElement?.replaceChild(input, text);
        input.focus();
    }
    private toggleNavigable(e: Event, item: LocalContent & { element?: HTMLElement })
    {
        cancelPropagation(e);

        this.history.add('overview', 'navigable', [{ element: item, from: item.navigable, to: !item.navigable }], true);
    }
    private togglePrivate(e: Event, item: LocalContent & { element?: HTMLElement })
    {
        cancelPropagation(e);

        this.history.add('overview', 'private', [{ element: item, from: item.private, to: !item.private }], true);
    }
    private setupDnD(): CleanupFn
    {
        return combine(...this.tree.tree.accumulate(this.dragndrop.bind(this)), monitorForElements({
            onDrop: ({ location }) => {
                if (location.initial.dropTargets.length === 0)
                    return;
                if (location.current.dropTargets.length === 0)
                    return;

                const target = location.current.dropTargets[0];
                const instruction = extractInstruction(target.data);

                if (instruction !== null)
                    this.updateTree(instruction, location.initial.dropTargets[0].data.id as string, target.data.id as string);
            },
        }), autoScrollForElements({
            element: this.tree.container,
        }));
    }
    private dragndrop(item: Omit<LocalContent & { element?: HTMLElement, cleanup?: () => void }, "content">, depth: number, parent?: Omit<LocalContent & { element?: HTMLElement }, "content">): CleanupFn
    {
        item.cleanup && item.cleanup();

        let opened = false, draggedOpened = false;
        const element = item.element!;
        item.cleanup = combine(draggable({
            element,
            onDragStart: () => {
                element.classList.toggle('opacity-50', true);
                opened = this.tree.opened(item)!;
                this.tree.toggle(item, false);
            },
            onDrop: () => {
                element.classList.toggle('opacity-50', false);
                this.tree.toggle(item, opened);
            },
            canDrag: ({ element }) => {
                return !element.querySelector('input[type="text"]');
            }
        }),

        dropTargetForElements({
            element,
            getData: ({ input }) => {
                const data = { id: item.id };

                return attachInstruction(data, {
                    input,
                    element,
                    indentPerLevel: 16,
                    currentLevel: depth,
                    mode: !!(item as Recursive<typeof item>).children ? 'expanded' : parent ? ((parent as Recursive<typeof item>).children!.length === item.order + 1 ? 'last-in-group' : 'standard') : this.tree.tree.flatten.slice(-1)[0] === item ? 'last-in-group' : 'standard',
                    block: [],
                })
            },
            canDrop: ({ source }) => {
                return source.data.id !== getPath(item);
            },
            onDrag: ({ self }) => {
                const instruction = extractInstruction(self.data) as Instruction;

                if(instruction)
                {
                    if('currentLevel' in instruction) this.instruction.style.width = `calc(100% - ${instruction.currentLevel / 2 - 1.5}em)`;

                    this.instruction.classList.toggle('!border-b-4', instruction?.type === 'reorder-below');
                    this.instruction.classList.toggle('!border-t-4', instruction?.type === 'reorder-above');
                    this.instruction.classList.toggle('!border-4', instruction?.type === 'make-child' || instruction?.type === 'reparent');

                    if(this.instruction.parentElement === null) element.appendChild(this.instruction);
                }
            },
            onDragEnter: () => {
                draggedOpened = this.tree.opened(item)!;
                this.tree.toggle(item, true);
            },
            onDragLeave: () => {
                this.tree.toggle(item, draggedOpened);
                this.instruction.remove();
            },
            onDrop: () => {
                this.tree.toggle(item, true);
                this.instruction.remove();
            },
            getIsSticky: () => true,
        }));

        return item.cleanup;
    }
    private updateTree(instruction: Instruction, source: string, target: string)
    {
        const sourceItem = this.tree.tree.find(source);
        const targetItem = this.tree.tree.find(target);

        if(!sourceItem || !targetItem || instruction.type === 'instruction-blocked')
            return;

        const from = { parent: (sourceItem as Recursive<typeof targetItem>).parent, order: sourceItem.order };

        if (source === target)
            return;

        if (instruction.type === 'reorder-above')
            this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order }}], true);

        if (instruction.type === 'reorder-below')
            this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: (targetItem as Recursive<typeof targetItem>).parent, order: targetItem!.order + 1 }}], true);

        if (instruction.type === 'make-child' && targetItem.type === 'folder')
            this.history.add('overview', 'move', [{ element: sourceItem, from: from, to: { parent: targetItem, order: 0 }}], true);
    }
    private render<T extends FileType>(item: LocalContent<T>): Node
    {
        return handlers[item.type].renderEditor(item);
    }
    private select(item?: LocalContent & { element?: HTMLElement })
    {
        if(this.selected && item)
        {
            console.log(this.selected.content);
            Content.save(this.selected);
        }
        if(this.selected === item)
        {
            item?.element!.classList.remove('text-accent-blue');
            this.selected = undefined;
        }
        else
        {
            this.selected?.element!.classList.remove('text-accent-blue');
            item?.element!.classList.add('text-accent-blue');
            this.selected = item;
        }

        this.container.firstElementChild!.replaceChildren();
        this.selected && this.container.firstElementChild!.appendChild(this.render(this.selected) as HTMLElement);
    }
    unmount()
    {
        this.cleanup();
    }
}

export function getPath(item: Recursive<Omit<LocalContent, 'content'>>): string
export function getPath(item: Omit<LocalContent, 'content'>): string
export function getPath(item: any): string
{
    if(item.hasOwnProperty('parent') && item.parent !== undefined)
        return [getPath(item.parent), parsePath(item.title)].filter(e => !!e).join('/');
    else if(item.hasOwnProperty('parent'))
        return parsePath(item.title);
    else
        return parsePath(item.title) ?? item.path;
}