import React from 'react';
import { ICellModel } from '@jupyterlab/cells';
import { NotebookPanel } from '@jupyterlab/notebook';
import { IComm, IKernelConnection } from '@jupyterlab/services/lib/kernel/kernel';
import { ICommMsgMsg } from '@jupyterlab/services/lib/kernel/messages';
import { PanelLayout } from '@lumino/widgets';
import CurrentCellTracker from './current-cell';
import { CellWidget } from '../components';
import { ReactWidget } from '@jupyterlab/apputils';

import type { NotebookStore } from '../store/notebook';
export default class JupyterLabSparkMonitor {
    currentCellTracker: CurrentCellTracker;
    cellExecCountSinceSparkJobStart = 0;
    kernel?: IKernelConnection;

    /** Communication object with the kernel. */
    comm?: IComm;

    constructor(private notebookPanel: NotebookPanel, private notebookStore: NotebookStore) {
        this.createCellReactElements();
        this.currentCellTracker = new CurrentCellTracker(notebookPanel);
        this.kernel = (notebookPanel as any).session
            ? (this.notebookPanel as any).session.kernel
            : this.notebookPanel.sessionContext.session?.kernel;

        // Fixes Reloading the browser
        this.startComm();

        // Fixes Restarting the Kernel
        this.kernel?.statusChanged.connect((_, status) => {
            if (status === 'starting') {
                this.currentCellTracker.cellReexecuted = false;
                this.startComm();
            }
        });

        // listen for cell removed
        this.notebookPanel.content.model?.cells.changed.connect((_, data) => {
            if (data.type === 'remove') {
                data.oldValues.forEach((cell) => {
                    notebookStore.onCellRemoved(cell.id);
                });
            }
        });
    }

    createCellReactElements() {
        const createElementIfNotExists = (cellModel: ICellModel) => {
            if (cellModel.type === 'code') {
                const codeCell = this.notebookPanel.content.widgets.find((widget) => widget.model === cellModel);
                if (codeCell && !codeCell.node.querySelector('.sparkMonitorCellRoot')) {
                    const widget = ReactWidget.create(
                        React.createElement(CellWidget, {
                            notebookId: this.notebookPanel.id,
                            cellId: cellModel.id,
                        }),
                    );
                    widget.addClass('SparkMonitorCellWidget');

                    (codeCell.layout as PanelLayout).insertWidget(
                        2, // Insert the element below the input area based on position/index
                        widget,
                    );
                    codeCell.update();
                }
            }
        };

        const cells = this.notebookPanel.context.model.cells;

        // Ensure new cells created have a monitoring display
        cells.changed.connect((cells, changed) => {
            for (let i = 0; i < cells.length; i += 1) {
                createElementIfNotExists(cells.get(i));
            }
        });

        // Do it the first time
        for (let i = 0; i < cells.length; i += 1) {
            createElementIfNotExists(cells.get(i));
        }
    }

    toggleAll() {
        this.notebookStore.toggleHideAllDisplays();
    }

    startComm() {
        console.log('SparkMonitor: Starting Comm with kernel.');
        this.currentCellTracker.ready().then(() => {
            this.comm =
                'createComm' in (this.kernel || {})
                    ? this.kernel?.createComm('SparkMonitor')
                    : (this.kernel as any).connectToComm('SparkMonitor');
            if (!this.comm) {
                console.warn('SparkMonitor: Unable to connect to comm');
                return;
            }
            this.comm.open({ msgtype: 'openfromfrontend' });
            this.comm.onMsg = (message) => {
                this.handleMessage(message);
            };
            this.comm.onClose = (message) => {
                // noop
            };
            console.log('SparkMonitor: Connection with comms established');
        });
    }

    onSparkJobStart(data: any) {
        const cell = this.currentCellTracker.getActiveCell();
        if (!cell) {
            console.warn('SparkMonitor: Job started with no running cell.');
            return;
        }
        // See if we have a new execution. If it's new (a cell has been run again) we need to clear the cell monitor
        const newExecution = this.currentCellTracker.getNumCellsExecuted() > this.cellExecCountSinceSparkJobStart;
        if (newExecution) {
            this.cellExecCountSinceSparkJobStart = this.currentCellTracker.getNumCellsExecuted();
            this.notebookStore.onCellExecutedAgain(cell.model.id);
        }
        this.notebookStore.onSparkJobStart(cell.model.id, data);
    }

    onSparkStageSubmitted(data: any) {
        const cell = this.currentCellTracker.getActiveCell();
        if (!cell) {
            console.warn('SparkMonitor: Stage started with no running cell.');
            return;
        }
        this.notebookStore.onSparkStageSubmitted(cell.model.id, data);
    }

    handleMessage(msg: ICommMsgMsg) {
        if (!msg.content.data.msgtype) {
            console.warn('SparkMonitor: Unknown message');
        }
        if (msg.content.data.msgtype === 'fromscala') {
            const data: any = JSON.parse(msg.content.data.msg as string);
            switch (data.msgtype) {
                case 'sparkJobStart':
                    this.onSparkJobStart(data);
                    break;
                case 'sparkJobEnd':
                    this.notebookStore.onSparkJobEnd(data);
                    break;
                case 'sparkStageSubmitted':
                    this.onSparkStageSubmitted(data);
                    break;
                case 'sparkStageCompleted':
                    this.notebookStore.onSparkStageCompleted(data);
                    break;
                case 'sparkStageActive':
                    this.notebookStore.onSparkStageActive(data);
                    break;
                case 'sparkTaskStart':
                    this.notebookStore.onSparkTaskStart(data);
                    break;
                case 'sparkTaskEnd':
                    this.notebookStore.onSparkTaskEnd(data);
                    break;
                case 'sparkApplicationStart':
                    this.notebookStore.onSparkApplicationStart(data);
                    break;
                case 'sparkApplicationEnd':
                    // noop
                    break;
                case 'sparkExecutorAdded':
                    this.notebookStore.onSparkExecutorAdded(data);
                    break;
                case 'sparkExecutorRemoved':
                    this.notebookStore.onSparkExecutorRemoved(data);
                    break;
                default:
                    console.warn('SparkMonitor: Unknown message');
                    break;
            }
        }
    }
}