import {Component, EventEmitter, Input, OnInit, Output, ViewChildren} from '@angular/core';
import {debug} from '../../utility';

/**
 * A recursive tree listing component.
 */
@Component({
    selector: 'app-tree-root',
    templateUrl: './tree-root.component.html',
    styleUrls: ['./tree-root.component.scss'],
})
export class TreeRootComponent implements OnInit {
    /** The immediate children of this component. */
    @ViewChildren('childComponents')
    protected childComponents;

    /** How deeply nested this component is in the tree. */
    @Input()
    public nestingLevel = 0;

    /** Field on the listing record with the display string. */
    @Input()
    public displayField = 'name';

    /** Field on the listing record that contains the children of an item. */
    @Input()
    public childrenField = 'children';

    /** Optionally, field on the listing record that contains the <i> icon class. */
    @Input()
    public iconClassField?: string;

    /** The nested listing items. */
    @Input()
    public items: any[] = [];

    /** @private Set by the parent component when recursively nested. */
    @Input()
    public parent?: TreeRootComponent;

    /** @private Item object that created this listing when recursively nested. */
    @Input()
    public parentItem?: any;

    /** Emits the selected item, when top-level tree. */
    @Output()
    public itemSelected: EventEmitter<{ event: MouseEvent, item: any }> = new EventEmitter<{event: MouseEvent; item: any}>();

    /** Emits the activated item, when top-level tree. */
    @Output()
    public itemActivated: EventEmitter<{ event: MouseEvent, item: any }> = new EventEmitter<{event: MouseEvent; item: any}>();

    /** Emits the right-clicked item, when top-level tree. */
    @Output()
    public itemRightClicked: EventEmitter<{ event: MouseEvent, item: any }> = new EventEmitter<{event: MouseEvent; item: any}>();

    /** The selected item record at this level, if one exists. */
    public selectedItem?: any;

    /** The current tree filter value, if one exists. */
    protected filterValue?: string;

    /** Called on initialization. */
    ngOnInit() {
        if ( !this.parent ) {
            debug('Top-level tree root component:', this);
        }
    }

    /** True if we're in dark mode. */
    public isDark() {
        return document.body.classList.contains('dark');
    }

    /** Recursively filter the tree using the given quick-filter string. */
    public filterTree(filterValue?: string) {
        this.setFilterRecursively(filterValue);
    }

    /** Emit an item selected event, or bubble up to the parent. */
    public emitItemSelected(event: MouseEvent, item: any) {
        if ( this.parent ) {
            return this.parent.emitItemSelected(event, item);
        }

        this.itemSelected.emit({
            event,
            item,
        });
    }

    /** Emit an item activated event, or bubble up to the parent. */
    public emitItemActivated(event: MouseEvent, item: any) {
        if ( this.parent ) {
            return this.parent.emitItemActivated(event, item);
        }

        this.itemActivated.emit({
            event,
            item,
        });
    }

    /** Emit an item right-clicked event, or bubble up to the parent. */
    public emitItemRightClicked(event: MouseEvent, item: any) {
        if ( this.parent ) {
            return this.parent.emitItemRightClicked(event, item);
        }

        this.itemRightClicked.emit({
            event,
            item,
        });
    }

    /** Wrapper for checking arrays for use in NG templates. */
    public isArray(something: unknown): boolean {
        return Array.isArray(something);
    }

    /**
     * Returns true if an item has nested children.
     * @param item
     * @param includeHidden - if true, will include children hidden by the quick-filter
     */
    public hasNesting(item: any, includeHidden = false): boolean {
        const hasUnfilteredNesting = item && this.isArray(item[this.childrenField]) && item[this.childrenField].length;
        if ( !this.filterValue || includeHidden ) {
            return hasUnfilteredNesting;
        }

        return hasUnfilteredNesting && item[this.childrenField].some(child => !child.__app_tree_comp_is_hidden);
    }

    /** Called when an item expand/collapse arrow is clicked. */
    public onItemHandleClick(event: MouseEvent, item: any) {
        if ( this.hasNesting(item) ) {
            this.setExpansionRecursively(item, !item.__app_tree_comp_is_collapsed);
        }
    }

    /** Called when an item is clicked. */
    public onItemClick(event: MouseEvent, item: any) {
        this.clearSelection();
        this.selectedItem = item;
        this.emitItemSelected(event, item);
    }

    /** Called when an item is double-clicked. */
    public onItemDoubleClick(event: MouseEvent, item: any) {
        this.emitItemActivated(event, item);
    }

    /** Called when an item is right-clicked. */
    public onItemRightClick(event: MouseEvent, item: any) {
        this.clearSelection();
        this.selectedItem = item;
        this.emitItemRightClicked(event, item);
    }

    /** Recursively clear the selection from the entire tree, calling children and bubbling up to parent. */
    public clearSelection(from?: TreeRootComponent) {
        this.selectedItem = undefined;

        if ( this.childComponents ) {
            for ( const child of this.childComponents ) {
                if ( child === from ) {
                    continue;
                }

                child.clearSelection(this);
            }
        }

        if ( this.parent ) {
            if ( this.parent !== from ) {
                this.parent.clearSelection(this);
            }
        }
    }

    /**
     * Recursively set the filter on this subtree.
     * Returns true if any of the children of this level match the filter.
     * @param filterValue
     * @param from
     * @protected
     */
    protected setFilterRecursively(filterValue?: string, from?: TreeRootComponent): boolean {
        this.filterValue = filterValue;

        let anyChildMatchesFilter = false;
        for ( const item of this.items ) {
            item.__app_tree_comp_is_hidden = this.getFilterHiddenStatusForItem(item);
            anyChildMatchesFilter = anyChildMatchesFilter || !item.__app_tree_comp_is_hidden;
        }

        return anyChildMatchesFilter;
    }

    /**
     * Returns true if the given item should be hidden with the current quick-filter.
     * @param item
     * @protected
     */
    protected getFilterHiddenStatusForItem(item: any): boolean {
        const itemMatchesFilter = (
            !this.filterValue
            || (
                item[this.displayField]?.trim()?.toLowerCase()?.includes(this.filterValue?.trim()?.toLowerCase())
            )
        );

        if ( !this.hasNesting(item, true) ) {
            return !itemMatchesFilter;
        }

        let anyChildMatchesFilter = false;
        const childTree = this.getChildComponentForItem(item);
        if ( childTree ) {
            anyChildMatchesFilter = childTree.setFilterRecursively(this.filterValue, this);
        } else {
            debug('Unable to find childTree for item:', item[this.displayField]);
        }

        return !(!this.filterValue || itemMatchesFilter || anyChildMatchesFilter);
    }

    /**
     * Get the TreeRootComponent instance for some item's children, if it exists.
     * @param item
     * @protected
     */
    protected getChildComponentForItem(item: any): TreeRootComponent | undefined {
        return [...(this.childComponents || [])]?.find(child => {
            return child.parentItem === item;
        });
    }

    /**
     * Expand or collapse an item recursively.
     * @param item
     * @param isCollapsed
     */
    public setExpansionRecursively(item: any, isCollapsed: boolean) {
        item.__app_tree_comp_is_collapsed = isCollapsed;
        if ( this.hasNesting(item, true) && isCollapsed ) {
            for ( const child of item.children ) {
                this.setExpansionRecursively(child, isCollapsed);
            }
        }
    }
}
