import * as React from 'react'
import {
    FileTree,
    Directory,
    FileEntry,
    ItemType,
    IFileTreeHandle,
    WatchEvent,
    FileType,
    IItemRendererProps,
    FileOrDir
} from 'react-aspen'
import { Decoration, TargetMatchMode } from 'aspen-decorations'
import { FileTreeItem } from '../FileTreeItem'
import { Notificar, DisposablesComposite } from 'notificar'
import { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from '../types'
import { KeyboardHotkeys } from '../services/keyboardHotkeys'
import { TreeModelX } from '../TreeModelX'
import AutoSizer from "react-virtualized-auto-sizer";

export class FileTreeX extends React.Component<IFileTreeXProps> {
    private fileTreeHandle: IFileTreeXHandle
    private activeFileDec: Decoration
    private pseudoActiveFileDec: Decoration
    private activeFile: FileOrDir
    private pseudoActiveFile: FileOrDir
    private wrapperRef: React.RefObject<HTMLDivElement> = React.createRef()
    private events: Notificar<FileTreeXEvent>
    private disposables: DisposablesComposite
    private keyboardHotkeys: KeyboardHotkeys
    private fileTreeEvent: IFileTreeXTriggerEvents
    constructor(props: IFileTreeXProps) {
        super(props)
        this.events = new Notificar()
        this.disposables = new DisposablesComposite()
        this.activeFileDec = new Decoration('active')
        this.pseudoActiveFileDec = new Decoration('pseudo-active')
    }

    render() {
        const { height, width, model, disableCache } = this.props
        const { decorations } = model

        return <div
            onKeyDown={this.handleKeyDown}
            className='file-tree'
            onBlur={this.handleBlur}
            onClick={this.handleClick}
            onScroll={this.props.onScroll}
            ref={this.wrapperRef}
            style={{
              height: height ? height : "calc(100vh - 60px)",
              width: "100%",
              display: "flex",
              flexDirection: "column",
              flex: 1
            }}
            tabIndex={-1}>
            <AutoSizer onResize={this.onResize}>
            {({ width, height }) => (
            <FileTree
                height={height}
                width={width}
                model={model}
                itemHeight={FileTreeItem.renderHeight}
                onReady={this.handleTreeReady}
                disableCache={disableCache ? disableCache : false}
                >
                {(props: IItemRendererProps) => <FileTreeItem
                    item={props.item}
                    itemType={props.itemType}
                    decorations={decorations.getDecorations(props.item as any)}
                    onClick={this.handleItemClicked}
                    onDoubleClick={this.handleItemDoubleClicked}
                    onContextMenu={this.handleItemCtxMenu}
                    changeDirectoryCount={this.changeDirectoryCount}
                    events={this.events}/>}
            </FileTree>
            )}
            </AutoSizer>
        </div>
    }

    public componentDidMount() {
        for(let child of this.props.model.root.children) {
            this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'loaded', child)
        }
	}

    componentWillUnmount() {
        const { model } = this.props
        model.decorations.removeDecoration(this.activeFileDec)
        model.decorations.removeDecoration(this.pseudoActiveFileDec)
        this.disposables.dispose()
    }

    private handleTreeEvent = (event: IFileTreeXTriggerEvents) => {
        this.fileTreeEvent = this.props.onEvent
    }

    private handleTreeReady = (handle: IFileTreeHandle) => {
        const { onReady, model } = this.props
        const scrollDiv = this.wrapperRef.current?.querySelector('div')?.querySelector('div')
        if(this.props.onScroll) {
            scrollDiv?.addEventListener('scroll', (ev: any)=>this.props.onScroll?.(ev));
        }

        this.fileTreeHandle = {
            ...handle,
            getModel: () => this.props.model,
            getActiveFile: () => this.activeFile,
            setActiveFile: this.setActiveFile,
            getPseudoActiveFile: () => this.pseudoActiveFile,
            setPseudoActiveFile: this.setPseudoActiveFile,
            toggleDirectory: this.toggleDirectory,
            closeDir: this.closeDir,
            remove: this.removeDir,
            newFile: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewFile(dirOrPath as any)),
            newFolder: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewDirectory(dirOrPath as any)),
            onBlur: (callback) => this.events.add(FileTreeXEvent.OnBlur, callback),
            hasDirectFocus: () => this.wrapperRef.current === document.activeElement,
            first: this.first,
            parent: this.parent,
            hasParent: this.hasParent,
            isOpen: this.isOpen,
            isClosed: this.isClosed,
            itemData: this.itemData,
            children: this.children,
            getItemFromDOM: this.getItemFromDOM,
            getDOMFromItem: this.getDOMFromItem,
            onTreeEvents: (callback) => this.events.add(FileTreeXEvent.onTreeEvents, callback),
            addIcon: this.addIcon,
            addCssClass: this.addCssClass,
            create: this.create,
            remove: this.remove,
            update: this.update,
            refresh: this.refresh,
            setLabel: this.setLabel,
            unload: this.unload,
            deSelectActiveFile: this.deSelectActiveFile,
            resize: this.resize,
            showLoader: this.showLoader,
            hideLoader: this.hideLoader,
        }

        model.decorations.addDecoration(this.activeFileDec)
        model.decorations.addDecoration(this.pseudoActiveFileDec)

        this.disposables.add(this.fileTreeHandle.onDidChangeModel((prevModel: TreeModelX, newModel: TreeModelX) => {
            this.setActiveFile(null)
            this.setPseudoActiveFile(null)
            prevModel.decorations.removeDecoration(this.activeFileDec)
            prevModel.decorations.removeDecoration(this.pseudoActiveFileDec)
            newModel.decorations.addDecoration(this.activeFileDec)
            newModel.decorations.addDecoration(this.pseudoActiveFileDec)
        }))

        this.disposables.add(this.fileTreeHandle.onBlur(() => {
            this.setPseudoActiveFile(null)
        }))

        this.keyboardHotkeys = new KeyboardHotkeys(this.fileTreeHandle)

        if (typeof onReady === 'function') {
            onReady(this.fileTreeHandle)
        }
    }

    private setActiveFile = async (fileOrDirOrPath: FileOrDir | string, ensureVisible, align): Promise<void> => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === this.props.model.root) { return }
        if (this.activeFile !== fileH) {
            if (this.activeFile) {
                this.activeFileDec.removeTarget(this.activeFile)
            }
            if (fileH) {
                this.activeFileDec.addTarget(fileH as any, TargetMatchMode.Self)
            }
            this.activeFile = fileH
            this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH)

            if (fileH && ensureVisible === true) {
                const alignTree = align !== undefined && align !== null ? align : 'auto'
                await this.fileTreeHandle.ensureVisible(fileH, alignTree)
            }
        }
    }

    private ensureVisible = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH) {
            await this.fileTreeHandle.ensureVisible(fileH)
        }
    }

    private deSelectActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === this.props.model.root) { return }
        if (this.activeFile === fileH) {
            this.activeFileDec.removeTarget(this.activeFile)
            this.activeFile = null
        }
    }

    private setPseudoActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === this.props.model.root) { return }
        if (this.pseudoActiveFile !== fileH) {
            if (this.pseudoActiveFile) {
                this.pseudoActiveFileDec.removeTarget(this.pseudoActiveFile)
            }
            if (fileH) {
                this.pseudoActiveFileDec.addTarget(fileH as any, TargetMatchMode.Self)
            }
            this.pseudoActiveFile = fileH
        }
        if (fileH) {
            await this.fileTreeHandle.ensureVisible(fileH)
        }
        this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH)
    }

    private create = async (parentDir, itemData): Promise<void> => {
        if (parentDir == undefined || parentDir == null) {
            parentDir = this.props.model.root
        }
        const {create, model } = this.props
        const isOpen = parentDir.isExpanded
        let maybeFile = undefined

        if (isOpen && (parentDir._children == null || parentDir._children.length == 0)) {
            await this.fileTreeHandle.closeDirectory(parentDir as Directory)
        }
        if (!parentDir.isExpanded && (parentDir._children == null || parentDir._children.length == 0)) {
            await this.fileTreeHandle.openDirectory(parentDir as Directory)
        } else {
            await this.fileTreeHandle.openDirectory(parentDir as Directory)
            maybeFile = await create(parentDir.path, itemData)
            if (maybeFile && maybeFile.type && maybeFile.name) {
                model.root.inotify({
                    type: WatchEvent.Added,
                    directory: parentDir.path,
                    file: maybeFile,
                })
            }
        }
        this.changeDirectoryCount(parentDir)
        let newItem = parentDir._children.find((c) => c._metadata.data.id === itemData.id)
        newItem.resolvedPathCache = newItem.parent.path + "/" + newItem._metadata.data.id
        return newItem
    }

    private update = async (item, itemData): Promise<void> => {
        item._metadata.data = itemData
        await this.props.update(item.path, itemData)
        this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'updated', item)
    }

     private refresh = async (item): Promise<void> => {
         const {remove, model } = this.props
         const isOpen = item.isExpanded
         if (item.children && item.children.length > 0) {
             for(let entry of item.children) {
                 await this.remove(entry).then(val => {}, error => {console.warn("Error removing item")})
             }
         }
         if (isOpen) {
            const ref = FileTreeItem.itemIdToRefMap.get(item.id);

            if (ref) {
                this.showLoader(ref)
            }

             await this.fileTreeHandle.closeDirectory(item as Directory)
             await this.fileTreeHandle.openDirectory(item as Directory)
             await this.changeResolvePath(item as Directory)
             this.changeDirectoryCount(item)

            if (ref) {
                this.hideLoader(ref)
            }
         }
    }

    private unload = async (item): Promise<void> => {
         const {remove, model } = this.props
         const isOpen = item.isExpanded
         if (item.children && item.children.length > 0) {
             for(let entry of item.children) {
                 await this.remove(entry).then(val => {}, error => {console.warn(error)})
             }
         }
         if (isOpen) {
             await this.fileTreeHandle.closeDirectory(item as Directory)
             this.changeDirectoryCount(item)
         }
    }

    private remove = async (item): Promise<void> => {
        const {remove, model } = this.props
        const path = item.path
        await remove(path, false)
        const dirName = model.root.pathfx.dirname(path);
        const fileName = model.root.pathfx.basename(path);
        const parent = item.parent
        if (dirName === parent.path) {
            const item_1 = parent._children.find((c) => c._metadata && c._metadata.data.id === fileName);
            if (item_1) {
                parent.unlinkItem(item_1);
                if (parent._children.length == 0) { parent._children = null }
                this.changeDirectoryCount(parent)
                this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'removed', item)
            }
            else {
                console.warn("Item not found")
            }
        }
    }

    private first = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === undefined || fileH === null) { return this.props.model.root.children[0] }

        if (fileH.branchSize > 0) {
            return fileH.children[0]
        }
        return null
    }

    private parent = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === FileType.Directory || fileH === FileType.File) {
            return fileH.parent
        }

        return null
    }


    private hasParent = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === FileType.Directory || fileH === FileType.File) {
            return fileH.parent ? true : false
        }

        return false
    }

    private children = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === FileType.Directory) {
            return fileH.children
        }

        return null
    }


    private isOpen = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === FileType.Directory) {
            return fileH.isExpanded
        }

        return false
    }

     private isClosed = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === FileType.Directory || fileH === FileType.File) {
             return !fileH.isExpanded
        }

        return false
    }

    private itemData = async (fileOrDirOrPath: FileOrDir | string) => {
        const fileH = typeof fileOrDirOrPath === 'string'
            ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
            : fileOrDirOrPath

        if (fileH === FileType.Directory || fileH === FileType.File) {
             return fileH._metadata.data
        }

        return null
    }

    private setLabel = async(pathOrDir: string | Directory, label: string): Promise<void> => {
        const dir = typeof pathOrDir === 'string'
            ? await this.fileTreeHandle.getFileHandle(pathOrDir)
            : pathOrDir

        const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
        if (ref) {
            ref.style.background = 'none'
            const label$ = ref.querySelector('span.file-name') as HTMLDivElement

            if (label$) {
                if (typeof(label) == "object" && label.label) {
                    label = label.label
                }
                label$.innerHTML = label;
            }

        }

   }

    private changeDirectoryCount = async(pathOrDir: string | Directory): Promise<void> => {
        const dir = typeof pathOrDir === 'string'
            ? await this.fileTreeHandle.getFileHandle(pathOrDir)
            : pathOrDir

        if (dir.type === FileType.Directory && dir._metadata.data && dir._metadata.data.is_collection === true) {
            const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
            if (ref) {
                ref.style.background = 'none'
                const label$ = ref.querySelector('span.children-count') as HTMLDivElement
                if(dir.children && dir.children.length > 0) {
                    label$.innerHTML = "(" + dir.children.length + ")";
                } else {
                    label$.innerHTML = "";
                }
            }
        }

   }

    private closeDir = async (pathOrDir: string | Directory) => {
        const dir = typeof pathOrDir === 'string'
            ? await this.fileTreeHandle.getFileHandle(pathOrDir)
            : pathOrDir

        if (dir.type === FileType.Directory) {
            if ((dir as Directory).expanded) {
                this.fileTreeHandle.closeDirectory(dir as Directory)
                this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir)

            }
        }
    }

    private toggleDirectory = async (pathOrDir: string | Directory) => {
        const dir = typeof pathOrDir === 'string'
            ? await this.fileTreeHandle.getFileHandle(pathOrDir)
            : pathOrDir

        if (dir.type === FileType.Directory) {
            if ((dir as Directory).expanded) {
                this.fileTreeHandle.closeDirectory(dir as Directory)
                this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir)

            } else {
                const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
                if (ref) {
                    this.showLoader(ref)
                }

                await this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'beforeopen', dir)
                await this.fileTreeHandle.openDirectory(dir as Directory)
                await this.changeResolvePath(dir as Directory)

                if (ref) {
                    this.hideLoader(ref)
                }

                this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'opened', dir)
            }
        }
    }

    private addIcon = async (pathOrDir: string | Directory, icon) => {
        const dir = typeof pathOrDir === 'string'
            ? await this.fileTreeHandle.getFileHandle(pathOrDir)
            : pathOrDir

        const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
        if (ref) {
            const label$ = ref.querySelector('.file-label i') as HTMLDivElement
            label$.className = icon.icon;
        }

    }

    private addCssClass = async (pathOrDir: string | Directory, cssClass) => {
        const dir = typeof pathOrDir === 'string'
            ? await this.fileTreeHandle.getFileHandle(pathOrDir)
            : pathOrDir

        const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
        if (ref) {
            ref.classList.add(cssClass)
            if (!dir._metadata.data.extraClasses)
                dir._metadata.data.extraClasses = []

            dir._metadata.data.extraClasses.push(cssClass)
        }

    }

    private showLoader = (ref: HTMLDivElement) => {
        // get label ref and add loading class
        ref.style.background = 'none'
        const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement
        if (label$)  label$.classList.add("loading")
    }

    private hideLoader = (ref: HTMLDivElement) => {
        // remove loading class.
        ref.style.background = 'none'
        const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement
        if (label$) label$.classList.remove("loading")
    }

    private handleBlur = () => {
        this.events.dispatch(FileTreeXEvent.OnBlur)
    }

    private handleItemClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => {
        if (type === ItemType.Directory && ev.target.className.includes("directory-toggle")) {
            await this.toggleDirectory(item as Directory)
        }
        await this.setActiveFile(item as FileEntry)

    }

    private handleItemDoubleClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => {
        await this.toggleDirectory(item as Directory)
        await this.setActiveFile(item as FileEntry)

    }

    private getItemFromDOM = (clientReact) => {
        return FileTreeItem.refToItemIdMap.get(clientReact);
    }

    private getDOMFromItem = (item: FileOrDir) => {
        return FileTreeItem.itemIdToRefMap.get(item.id);
    }

    private handleClick = (ev: React.MouseEvent) => {
        // clicked in "blank space"
        if (ev.currentTarget === ev.target) {
            this.setPseudoActiveFile(null)
        }
    }

    private handleItemCtxMenu = (ev: React.MouseEvent, item: FileOrDir) => {
        return this.props.onContextMenu?.(ev, item);
    }

    private handleKeyDown = (ev: React.KeyboardEvent) => {
        return this.keyboardHotkeys.handleKeyDown(ev)
    }

    private onResize = (...args) => {
         if (this.wrapperRef.current != null) {
            this.resize()
         }
    }

    private resize = (scrollX, scrollY) => {
        const scrollXPos = scrollX ? scrollX : 0
        const scrollYPos = scrollY ? scrollY : this.props.model.state.scrollOffset
        const div = this.wrapperRef.current.querySelector('div').querySelector('div') as HTMLDivElement
        div.scroll(scrollXPos, scrollYPos)

    }

    private changeResolvePath = async (item: FileOrDir): Promise<void> => {
        // Change the path as per pgAdmin requirement: Item Id wise
        if (item.type === FileType.File) {
            item.resolvedPathCache = item.parent.path + "/" + item._metadata.data.id
        }
        if (item.type === FileType.Directory && item.children && item.children.length > 0) {
            for(let entry of item.children) {
                entry.resolvedPathCache = entry.parent.path + "/" + entry._metadata.data.id
            }
        }
    }
}

export { IFileTreeXHandle, IFileTreeXProps }
