// *****************************************************************************
// Copyright (C) 2023 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import * as React from '@theia/core/shared/react';
import { CellEditType, CellKind, NotebookCellsChangeType } from '../../common';
import { NotebookCellModel } from '../view-model/notebook-cell-model';
import { NotebookModel } from '../view-model/notebook-model';
import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory';
import { animationFrame, onDomEvent } from '@theia/core/lib/browser';
import { CommandMenu, CommandRegistry, DisposableCollection, MenuModelRegistry, nls } from '@theia/core';
import { NotebookCommands, NotebookMenus } from '../contributions/notebook-actions-contribution';
import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution';
import { NotebookContextManager } from '../service/notebook-context-manager';
import { NotebookViewModel } from '../view-model/notebook-view-model';

export interface CellRenderer {
    render(notebookData: NotebookModel, cell: NotebookCellModel, index: number): React.ReactNode
    renderSidebar(notebookModel: NotebookModel, cell: NotebookCellModel): React.ReactNode
    renderDragImage(cell: NotebookCellModel): HTMLElement
}

export function observeCellHeight(ref: HTMLDivElement | null, cell: NotebookCellModel): void {
    if (ref) {
        cell.cellHeight = ref?.getBoundingClientRect().height ?? 0;
        new ResizeObserver(entries =>
            cell.cellHeight = ref?.getBoundingClientRect().height ?? 0
        ).observe(ref);
    }
}

interface CellListProps {
    renderers: Map<CellKind, CellRenderer>;
    notebookModel: NotebookModel;
    notebookViewModel: NotebookViewModel;
    notebookContext: NotebookContextManager;
    toolbarRenderer: NotebookCellToolbarFactory;
    commandRegistry: CommandRegistry;
    menuRegistry: MenuModelRegistry;
}

interface NotebookCellListState {
    selectedCell?: NotebookCellModel;
    scrollIntoView: boolean;
    dragOverIndicator: { cell: NotebookCellModel, position: 'top' | 'bottom' } | undefined;
}

export class NotebookCellListView extends React.Component<CellListProps, NotebookCellListState> {

    protected toDispose = new DisposableCollection();

    protected static dragGhost: HTMLElement | undefined;
    protected cellListRef: React.RefObject<HTMLUListElement> = React.createRef();

    constructor(props: CellListProps) {
        super(props);
        this.state = { selectedCell: props.notebookViewModel.selectedCell, dragOverIndicator: undefined, scrollIntoView: true };
        this.toDispose.push(props.notebookModel.onDidAddOrRemoveCell(e => {
            if (e.newCellIds && e.newCellIds.length > 0) {
                this.setState({
                    ...this.state,
                    selectedCell: props.notebookViewModel.selectedCell,
                    scrollIntoView: true
                });
            } else {
                this.setState({
                    ...this.state,
                    selectedCell: props.notebookViewModel.selectedCell,
                    scrollIntoView: false
                });
            }
        }));

        this.toDispose.push(props.notebookModel.onDidChangeContent(events => {
            if (events.some(e => e.kind === NotebookCellsChangeType.Move)) {
                // When a cell has been moved, we need to rerender the whole component
                this.forceUpdate();
            }
        }));

        this.toDispose.push(props.notebookViewModel.onDidChangeSelectedCell(e => {
            this.setState({
                ...this.state,
                selectedCell: e.cell,
                scrollIntoView: e.scrollIntoView
            });
        }));

        this.toDispose.push(onDomEvent(document, 'focusin', () => {
            animationFrame().then(() => {
                if (!this.cellListRef.current) {
                    return;
                }
                let hasCellFocus = false;
                let hasFocus = false;
                if (this.cellListRef.current.contains(document.activeElement)) {
                    if (this.props.notebookViewModel.selectedCell) {
                        hasCellFocus = true;
                    }
                    hasFocus = true;
                }
                this.props.notebookContext.changeCellFocus(hasCellFocus);
                this.props.notebookContext.changeCellListFocus(hasFocus);
            });
        }));
    }

    override componentWillUnmount(): void {
        this.toDispose.dispose();
    }

    override render(): React.ReactNode {
        return <ul className='theia-notebook-cell-list' ref={this.cellListRef} onDragStart={e => this.onDragStart(e)}>
            {this.props.notebookModel.getVisibleCells()
                .map((cell, index) => {
                    const cellViewModel = this.props.notebookViewModel.cellViewModels.get(cell.handle);
                    return <React.Fragment key={'cell-' + cell.handle}>
                        <NotebookCellDivider
                            menuRegistry={this.props.menuRegistry}
                            isVisible={() => this.isEnabled()}
                            onAddNewCell={handler => this.onAddNewCell(handler, index)}
                            onDrop={e => this.onDrop(e, index)}
                            onDragOver={e => this.onDragOver(e, cell, 'top')} />
                        <CellDropIndicator visible={this.shouldRenderDragOverIndicator(cell, 'top')} />
                        <li className={'theia-notebook-cell' + (this.state.selectedCell === cell ? ' focused' : '') + (this.isEnabled() ? ' draggable' : '')}
                            onDragEnd={e => {
                                NotebookCellListView.dragGhost?.remove();
                                this.setState({ ...this.state, dragOverIndicator: undefined });
                            }}
                            onDragOver={e => this.onDragOver(e, cell)}
                            onDrop={e => this.onDrop(e, index)}
                            draggable={true}
                            tabIndex={-1}
                            data-cell-handle={cell.handle}
                            ref={ref => {
                                if (ref && cell === this.state.selectedCell && this.state.scrollIntoView) {
                                    ref.scrollIntoView({ block: 'nearest' });
                                    if (cell.cellKind === CellKind.Markup && !cellViewModel?.editing) {
                                        ref.focus();
                                    }
                                }
                            }}
                            onClick={e => {
                                this.setState({ ...this.state, selectedCell: cell });
                                this.props.notebookViewModel.setSelectedCell(cell, false);
                            }}
                        >
                            <div className='theia-notebook-cell-sidebar'>
                                <div className={'theia-notebook-cell-marker' + (this.state.selectedCell === cell ? ' theia-notebook-cell-marker-selected' : '')}></div>
                                {this.renderCellSidebar(cell)}
                            </div>
                            <div className='theia-notebook-cell-content'>
                                {this.renderCellContent(cell, index)}
                            </div>
                            {this.state.selectedCell === cell &&
                                this.props.toolbarRenderer.renderCellToolbar(NotebookCellActionContribution.ACTION_MENU, cell, {
                                    contextMenuArgs: () => [cell], commandArgs: () => [this.props.notebookModel, cell]
                                })
                            }
                        </li>
                        <CellDropIndicator visible={this.shouldRenderDragOverIndicator(cell, 'bottom')} />
                    </React.Fragment>;
                })
            }
            <NotebookCellDivider
                menuRegistry={this.props.menuRegistry}
                isVisible={() => this.isEnabled()}
                onAddNewCell={handler => this.onAddNewCell(handler, this.props.notebookModel.cells.length)}
                onDrop={e => this.onDrop(e, this.props.notebookModel.cells.length - 1)}
                onDragOver={e => this.onDragOver(e, this.props.notebookModel.cells[this.props.notebookModel.cells.length - 1], 'bottom')} />
        </ul>;
    }

    renderCellContent(cell: NotebookCellModel, index: number): React.ReactNode {
        const renderer = this.props.renderers.get(cell.cellKind);
        if (!renderer) {
            throw new Error(`No renderer found for cell type ${cell.cellKind}`);
        }
        return renderer.render(this.props.notebookModel, cell, index);
    }

    renderCellSidebar(cell: NotebookCellModel): React.ReactNode {
        const renderer = this.props.renderers.get(cell.cellKind);
        if (!renderer) {
            throw new Error(`No renderer found for cell type ${cell.cellKind}`);
        }
        return renderer.renderSidebar(this.props.notebookModel, cell);
    }

    protected onDragStart(event: React.DragEvent<HTMLElement>): void {
        event.stopPropagation();
        if (!this.isEnabled()) {
            event.preventDefault();
            return;
        }

        const cellHandle = (event.target as HTMLLIElement).getAttribute('data-cell-handle');

        if (!cellHandle) {
            throw new Error('Cell handle not found in element for cell drag event');
        }

        const index = this.props.notebookModel.getCellIndexByHandle(parseInt(cellHandle));
        const cell = this.props.notebookModel.cells[index];

        NotebookCellListView.dragGhost = document.createElement('div');
        NotebookCellListView.dragGhost.classList.add('theia-notebook-drag-ghost-image');
        NotebookCellListView.dragGhost.appendChild(this.props.renderers.get(cell.cellKind)?.renderDragImage(cell) ?? document.createElement('div'));
        document.body.appendChild(NotebookCellListView.dragGhost);
        event.dataTransfer.setDragImage(NotebookCellListView.dragGhost, -10, 0);

        event.dataTransfer.setData('text/theia-notebook-cell-index', index.toString());
        event.dataTransfer.setData('text/plain', this.props.notebookModel.cells[index].source);
    }

    protected onDragOver(event: React.DragEvent<HTMLLIElement>, cell: NotebookCellModel, position?: 'top' | 'bottom'): void {
        if (!this.isEnabled()) {
            return;
        }
        event.preventDefault();
        event.stopPropagation();
        // show indicator
        this.setState({ ...this.state, dragOverIndicator: { cell, position: position ?? event.nativeEvent.offsetY < event.currentTarget.clientHeight / 2 ? 'top' : 'bottom' } });
    }

    protected isEnabled(): boolean {
        return !Boolean(this.props.notebookModel.readOnly);
    }

    protected onDrop(event: React.DragEvent<HTMLLIElement>, dropElementIndex: number): void {
        if (!this.isEnabled()) {
            this.setState({ dragOverIndicator: undefined });
            return;
        }
        const index = parseInt(event.dataTransfer.getData('text/theia-notebook-cell-index'));
        const isTargetBelow = index < dropElementIndex;
        let newIdx = this.state.dragOverIndicator?.position === 'top' ? dropElementIndex : dropElementIndex + 1;
        newIdx = isTargetBelow ? newIdx - 1 : newIdx;
        if (index !== undefined && index !== dropElementIndex) {
            this.props.notebookModel.applyEdits([{
                editType: CellEditType.Move,
                length: 1,
                index,
                newIdx
            }], true);
        }
        this.setState({ ...this.state, dragOverIndicator: undefined });
    }

    protected onAddNewCell(handler: (...args: unknown[]) => void, index: number): void {
        if (this.isEnabled()) {
            this.props.commandRegistry.executeCommand(NotebookCommands.CHANGE_SELECTED_CELL.id, index - 1);
            handler(
                this.props.notebookModel,
                index
            );
        }
    }

    protected shouldRenderDragOverIndicator(cell: NotebookCellModel, position: 'top' | 'bottom'): boolean {
        return this.isEnabled() &&
            this.state.dragOverIndicator !== undefined &&
            this.state.dragOverIndicator.cell === cell &&
            this.state.dragOverIndicator.position === position;
    }

}

export interface NotebookCellDividerProps {
    isVisible: () => boolean;
    onAddNewCell: (createCommand: (...args: unknown[]) => void) => void;
    onDrop: (event: React.DragEvent<HTMLLIElement>) => void;
    onDragOver: (event: React.DragEvent<HTMLLIElement>) => void;
    menuRegistry: MenuModelRegistry;
}

export function NotebookCellDivider({ isVisible, onAddNewCell, onDrop, onDragOver, menuRegistry }: NotebookCellDividerProps): React.JSX.Element {
    const [hover, setHover] = React.useState(false);

    const menuPath = NotebookMenus.NOTEBOOK_MAIN_TOOLBAR_CELL_ADD_GROUP; // we contribute into this menu, so it will exist
    const menuItems: CommandMenu[] = menuRegistry.getMenu(menuPath)!.children.filter(item => CommandMenu.is(item)).map(item => item as CommandMenu);

    const renderItem = (item: CommandMenu): React.ReactNode => {
        const execute = (...args: unknown[]) => {
            if (CommandMenu.is(item)) {
                item.run([...menuPath, item.id], ...args);
            }
        };
        return <button
            key={item.id}
            className='theia-notebook-add-cell-button'
            onClick={() => onAddNewCell(execute)}
            title={nls.localizeByDefault(`Add ${item.label} Cell`)}
        >
            <div className={item.icon + ' theia-notebook-add-cell-button-icon'} />
            <div className='theia-notebook-add-cell-button-text'>{item.label}</div>
        </button>;
    };

    return <li className='theia-notebook-cell-divider' onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} onDrop={onDrop} onDragOver={onDragOver}>
        {hover && isVisible() && <div className='theia-notebook-add-cell-buttons'>
            {menuItems.map((item: CommandMenu) => renderItem(item))}
        </div>}
    </li>;
}

function CellDropIndicator(props: { visible: boolean }): React.JSX.Element {
    return <div className='theia-notebook-cell-drop-indicator' style={{ visibility: props.visible ? 'visible' : 'hidden' }} />;
}
