import { Content, type LocalContent } from "./content.util";
import { dom } from "./dom.util";
import { clamp } from "./general.util";

export type Recursive<T> = T & {
    children?: T[];
    parent?: T;
};
export class Tree<T extends Omit<LocalContent, 'content'>>
{
    private _data:  Recursive<T>[];
    private _flatten: T[];

    constructor(data: T[])
    {
        this._data = data;
        this._flatten = this.accumulate(e => e);
    }

    remove(id: string)
    {
        const recursive = (data?: Recursive<T>[], parent?: T) => data?.filter(e => e.id !== id)?.map((e, i) => {
            e.order = i;
            e.children = recursive(e.children as T[], e);
            return e;
        });

        this._data = recursive(this._data)!;
        this._flatten = this.accumulate(e => e);
    }
    insertAt(item: Recursive<T>, pos: number)
    {
        const parent = item.parent ? item.parent.id : undefined;
        const recursive = (data?: Recursive<T>[]) => data?.flatMap(e => {
            if(e.id === parent)
            {
                e.children = e.children ?? [];
                e.children.splice(clamp(pos, 0, e.children.length), 0, item);
            }
            else if(e.type === 'folder')
                e.children = recursive(e.children as T[]);

            return e;
        }).map((e, i) => { e.order = i; return e; });

        if(!parent || parent === '')
        {
            this._data.splice(pos, 0, item);
            this._data.forEach((e, i) => e.order = i);
        }
        else
            this._data = recursive(this._data)!;

        this._flatten = this.accumulate(e => e);
    }
    find(id: string): T | undefined 
    {
        const recursive = (data?: Recursive<T>[]): T | undefined => {
            if(!data)
                return;

            for(const e of data)
            {
                if(e.id === id)
                    return e;

                const result = recursive(e.children as T[]);

                if(result)
                    return result;
            }
        };
        
        return recursive(this._data);
    }
    search(prop: keyof T, value: string): T[] 
    {
        const recursive = (data?: Recursive<T>[]): T[] => {
            if(!data)
                return [];

            const arr = [];

            for(const e of data)
            {
                if(e[prop] === value)
                    arr.push(e);
                else
                    arr.push(...recursive(e.children as T[]));
            }

            return arr;
        };
        
        return recursive(this._data);
    }
    subset(id: string): Tree<T> | undefined
    {
        const subset = this.find(id);
        return subset ? new Tree([subset]) : undefined;
    }
    each(callback: (item: T, depth: number, parent?: T) => void)
    {
        const recursive = (depth: number, data?: Recursive<T>[], parent?: T) => data?.forEach(e => { callback(e, depth, parent); recursive(depth + 1, e.children as T[], e) });

        recursive(1, this._data);
    }
    accumulate(callback: (item: T, depth: number, parent?: T) => any): any[]
    {
        const recursive = (depth: number, data?: Recursive<T>[], parent?: T): any[] => data?.flatMap(e => [callback(e, depth, parent), ...recursive(depth + 1, e.children as T[], e)]) ?? [];

        return recursive(1, this._data);
    }
    get data()
    {
        return this._data;
    }
    get flatten()
    {
        return this._flatten;
    }
}
export class TreeDOM
{
    container: HTMLElement;
    tree: Tree<Omit<LocalContent & { element?: HTMLElement }, "content">>;

    private filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined;
    private folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement;
    private leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement;

    constructor(folder: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement, leaf: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => HTMLElement, filter?: (item: Recursive<Omit<LocalContent, 'content'>>, depth: number) => boolean | undefined)
    {
        this.tree = new Tree(Content.tree);

        this.filter = filter;
        this.folder = folder;
        this.leaf = leaf;

        const elements = this.tree.accumulate(this.render.bind(this));
        this.container = dom('div', { class: 'list-none select-none text-light-100 dark:text-dark-100 text-sm ps-2' }, elements);
    }
    render(item: Recursive<Omit<LocalContent & { element?: HTMLElement }, "content">>, depth: number): HTMLElement | undefined
    {
        if(this.filter && !(this.filter(item, depth) ?? true))
            return;

        if(item.type === 'folder')
        {
            let folded = false;
            if(item.element)
                folded = item.element.getAttribute('data-state') === 'open';
            item.element = this.folder(item, depth);

            if(!!item.parent) item.element.classList.toggle('hidden', item.parent.element!.getAttribute('data-state') === 'closed' || item.parent.element!.classList.contains('hidden'));
            item.element.setAttribute('data-state', folded ? 'open' : 'closed');
            item.element.addEventListener('click', () => this.toggle(item));

            return item.element;
        }
        else
        {
            item.element = this.leaf(item, depth);

            if(!!item.parent) item.element.classList.toggle('hidden', item.parent.element!.getAttribute('data-state') === 'closed' || item.parent.element!.classList.contains('hidden'));

            return item.element;
        }
    }
    update()
    {
        this.container.replaceChildren(...this.tree.flatten.map(e => e.element!));
    }
    toggle(item?: Omit<LocalContent & { element?: HTMLElement }, 'content'>, state?: boolean)
    {
        if(item && item.type === 'folder')
        {
            const open = state ?? item.element!.getAttribute('data-state') !== 'open';
            item.element!.setAttribute('data-state', open ? 'open' : 'closed');

            new Tree([item]).each((e, _, parent) => {
                if(!parent)
                    return;
    
                e.element!.classList.toggle('hidden', !this.opened(parent) || parent.element!.classList.contains('hidden'));
            });
        }
    }
    opened(item?: Omit<LocalContent & { element?: HTMLElement }, 'content'>): boolean | undefined
    {
        return item ? item.element!.getAttribute('data-state') === 'open' : undefined;
    }
}