/*
 * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient
 *
 * http://rtsys.informatik.uni-kiel.de/kieler
 *
 * Copyright 2021 by
 * + Kiel University
 *   + Department of Computer Science
 *     + Real-Time and Embedded Systems Group
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 */

/** @jsx html */
import { inject, postConstruct } from 'inversify'
import { VNode } from 'snabbdom'
import {
    AbstractUIExtension,
    html, // eslint-disable-line @typescript-eslint/no-unused-vars
    IActionDispatcher,
    InitializeCanvasBoundsAction,
    Patcher,
    PatcherProvider,
    TYPES,
} from 'sprotty'
import { DISymbol } from '../di.symbols'
import { PinSidebarOption, RenderOptionsRegistry } from '../options/render-options-registry'
import { ShowSidebarAction, ToggleSidebarPanelAction } from './actions'
import { SidebarPanelRegistry } from './sidebar-panel-registry'
/* global document, HTMLElement */

/**
 * UIExtension that adds a sidebar to the Sprotty container. The content of the
 * sidebar is implemented by panels, which are provided separately. The sidebar
 * reacts to updates of the {@link SidebarPanelRegistry} and syncs the UI with
 * the registry state.
 */
export class Sidebar extends AbstractUIExtension {
    static readonly ID = 'sidebar'

    /** Snabbdom patcher function and VDom root */
    private patcher: Patcher

    private oldPanelContentRoot: VNode

    /**
     * Maximum width of all opened panels.
     */
    private maxWidth = 0

    @inject(TYPES.PatcherProvider) patcherProvider: PatcherProvider

    @inject(TYPES.IActionDispatcher) private actionDispatcher: IActionDispatcher

    @inject(DISymbol.SidebarPanelRegistry) private sidebarPanelRegistry: SidebarPanelRegistry

    @inject(DISymbol.RenderOptionsRegistry) private renderOptionsRegistry: RenderOptionsRegistry

    @postConstruct()
    init(): void {
        this.actionDispatcher.dispatch(ShowSidebarAction.create())
        this.patcher = this.patcherProvider.patcher

        // Update the panel if the registry state changes
        this.sidebarPanelRegistry.onChange(() => this.update())

        // Update the panel if the current panel requests an update
        this.sidebarPanelRegistry.allPanels.forEach((panel) => {
            panel.onUpdate(() => {
                if (panel.id === this.sidebarPanelRegistry.currentPanelID) this.update()
            })
        })
    }

    id(): string {
        return Sidebar.ID
    }

    containerClass(): string {
        return Sidebar.ID
    }

    update(): void {
        // Only update if the content was initialized, which is the case if a
        // VNode Root for the panel content is created.
        if (!this.oldPanelContentRoot) return

        const { currentPanel } = this.sidebarPanelRegistry
        // Reset fit to fit content to calculate the desired width at the end.
        document.documentElement.style.setProperty('--sidebar-width', 'fit-content')

        const content: VNode = (
            <div class-sidebar__content="true">
                <div class-sidebar__toggle-container="true">
                    {this.sidebarPanelRegistry.allPanels.map((panel) => (
                        <button
                            class-sidebar__toggle-button="true"
                            class-sidebar__toggle-button--active={this.sidebarPanelRegistry.currentPanelID === panel.id}
                            title={panel.title}
                            on-click={this.handlePanelButtonClick.bind(this, panel.id)}
                        >
                            {panel.icon}
                        </button>
                    ))}
                </div>
                <h3 class-sidebar__title="true">{currentPanel?.title ?? ''}</h3>
                <div class-sidebar__panel-content="true">{currentPanel?.render() ?? ''}</div>
            </div>
        )

        // Update panel content with efficient VDOM patching.
        this.oldPanelContentRoot = this.patcher(this.oldPanelContentRoot, content)

        // Show or hide the panel
        this.maxWidth = Math.max(this.maxWidth, this.containerElement.clientWidth)
        if (currentPanel) {
            this.containerElement.classList.add('sidebar--open')
            // Set width of sidebar to maximum width of all opened panels.
            document.documentElement.style.setProperty('--sidebar-width', `${this.maxWidth}px`)
        } else {
            this.containerElement.classList.remove('sidebar--open')
            // Reset sidebar width when closed
            document.documentElement.style.setProperty('--sidebar-width', '0px')
        }
        // Find the canvas element and dispatch an action to re-initialize the canvas bounds.
        // This works since the previous css property will correctly set the width of the canvas.
        if (content.elm instanceof HTMLElement) {
            const canvas = content.elm.parentElement?.parentElement
            if (canvas) {
                this.actionDispatcher.dispatch(
                    InitializeCanvasBoundsAction.create({
                        x: canvas.clientLeft,
                        y: canvas.clientTop,
                        width: canvas.clientWidth,
                        height: canvas.clientHeight,
                    })
                )
            }
        }
    }

    protected onBeforeShow(): void {
        this.update()
    }

    protected initializeContents(containerElement: HTMLElement): void {
        // Prepare the virtual DOM. Snabbdom requires an empty element.
        // Furthermore, the element is completely replaced by the panel on every update,
        // so we use an extra, empty element to ensure that we do not loose important attributes (such as classes).
        const panelContentRoot = document.createElement('div')
        this.oldPanelContentRoot = this.patcher(panelContentRoot, <div />)
        containerElement.appendChild(panelContentRoot)

        // Notice that an AbstractUIExtension only calls initializeContents once,
        // so this handler is also only registered once.
        this.addClickOutsideListenser(containerElement)
        this.addMouseLeaveListener(containerElement)
    }

    private handlePanelButtonClick(id: string) {
        this.actionDispatcher.dispatch(ToggleSidebarPanelAction.create(id))
    }

    /**
     * Register a click outside handler that hides the content when a user click outsides.
     * Using "mousedown" instead of "click" also hides the panel as soon as the user starts
     * dragging the diagram.
     */
    private addClickOutsideListenser(containerElement: HTMLElement): void {
        document.addEventListener('mousedown', (e) => {
            const { currentPanelID } = this.sidebarPanelRegistry

            // See for information on detecting "click outside": https://stackoverflow.com/a/64665817/7569889
            if (
                currentPanelID &&
                !e.composedPath().includes(containerElement) &&
                !this.renderOptionsRegistry.getValueOrDefault(PinSidebarOption)
            ) {
                this.actionDispatcher.dispatch(ToggleSidebarPanelAction.create(currentPanelID, 'hide'))
            }
        })
    }

    /**
     * Register a mouse left handler that hides the content if the mouse leaves the sidebar.
     */
    private addMouseLeaveListener(containerElement: HTMLElement): void {
        containerElement.addEventListener('mouseleave', (e) => {
            const { currentPanelID } = this.sidebarPanelRegistry
            // Check if the mouse really left the bounding box of the container element.
            // This is necessary because the mouseleave event is also triggered when the mouse
            // "leaves" the sidebar to enter a tooltip. Take a small margin into account to
            // avoid an issue where the mouseleave event is triggered it is still inside the
            // container element.
            const rect = containerElement.getBoundingClientRect()
            if (e.x <= rect.left + 2 || e.x >= rect.right - 2 || e.y <= rect.top + 2 || e.y >= rect.bottom - 2) {
                if (currentPanelID && !this.renderOptionsRegistry.getValueOrDefault(PinSidebarOption)) {
                    this.actionDispatcher.dispatch(ToggleSidebarPanelAction.create(currentPanelID, 'hide'))
                }
            }
        })
    }
}
