/* eslint-disable max-len */
import * as grok from 'datagrok-api/grok';
import * as DG from 'datagrok-api/dg';
import * as ui from 'datagrok-api/ui';

import wu from 'wu';
import {Observable, Subject} from 'rxjs';

import {NglGlAux, NglGlProps} from '@datagrok-libraries/bio/src/viewers/ngl-gl-service';
import {IBiostructureViewer} from '@datagrok-libraries/bio/src/viewers/molstar-viewer';
import {DockingRole, DockingTags} from '@datagrok-libraries/bio/src/viewers/molecule3d';
import {testEvent} from '@datagrok-libraries/utils/src/test';
import {CellRendererBackAsyncBase, RenderServiceBase} from '@datagrok-libraries/bio/src/utils/cell-renderer-async-base';
import {getGridCellColTemp} from '@datagrok-libraries/bio/src/utils/cell-renderer-back-base';

import {IPdbGridCellRenderer} from './types';
import {_getNglGlService} from '../package-utils';
import {findLinkedSmilesColName, clearAtomPickerHighlights} from './mol3d-link';

import {_package,} from '../package';
import {LruCache} from 'datagrok-api/dg';

export const enum Temps {
  renderer = '.renderer.pdb',
}

export class PdbGridCellRendererBack extends CellRendererBackAsyncBase<NglGlProps, NglGlAux>
  implements IPdbGridCellRenderer {
  constructor(
    gridCol: DG.GridColumn | null,
    tableCol: DG.Column<string>
  ) {
    super(gridCol, tableCol, _package.logger, true);
  }

  override get minHeight(): number { return 30; }
  override get minWidth(): number { return 30; }

  protected getRenderService(): RenderServiceBase<NglGlProps, NglGlAux> {
    return _getNglGlService();
  }

  protected override getRenderTaskProps(
    gridCell: DG.GridCell, backColor: number, width: number, height: number
  ): NglGlProps {
    return new NglGlProps(gridCell.cell.value, backColor, width, height);
  }

  protected override storeAux(_gridCell: DG.GridCell, _aux: NglGlAux): void {}

  onClick(gridCell: DG.GridCell, _e: MouseEvent): void {
    this.createViewer(gridCell).then(async ({tview, viewer}) => {
      if (tview && viewer) {
        const currentViewer = wu(tview.viewers).find((v) => {
          return v.type === 'Biostructure' || v.type === 'NGL';
        }) as DG.Viewer & IBiostructureViewer;

        if (!currentViewer)
          tview.dockManager.dock(viewer.root, DG.DOCK_TYPE.RIGHT, null, 'Biostructure Viewer', 0.3);
        else {
          // Update existing viewer
          await new Promise<void>((resolve) => {
            testEvent(viewer!.onRendered, () => {
              this._onClicked.next();
              resolve();
            }, () => {
              currentViewer.setOptions({pdb: gridCell.cell.value});
              currentViewer.invalidate('onClick()'); // To trigger viewer.onRendered
            }, 10000);
          });
        }
      }
    });
  }

  async createViewer(gridCell: DG.GridCell, view?: DG.TableView): Promise<{ tview: DG.TableView | undefined, viewer: any }> {
    const df: DG.DataFrame = gridCell.grid.dataFrame;
    const tableCol: DG.Column | null = gridCell.tableColumn;
    if (!tableCol)
      return {tview: undefined, viewer: null};

    const dockingRole: string = tableCol.getTag(DockingTags.dockingRole);
    const value: string = gridCell.cell.value;

    const tview: DG.TableView | undefined = view ?? wu(grok.shell.tableViews)
      .find((tv) => tv.dataFrame.id === df.id);

    if (!tview)
      return {tview: undefined, viewer: null};

    let viewer: (DG.Viewer & IBiostructureViewer) | undefined;

    switch (dockingRole) {
    case DockingRole.ligand:
    case DockingRole.target:
    default: {
      viewer = await df.plot.fromType('Biostructure', {pdb: value}) as DG.Viewer & IBiostructureViewer;
      await new Promise<void>((resolve) => {
        testEvent(viewer!.onRendered, () => {
          this._onClicked.next();
          resolve();
        }, () => {
            viewer!.invalidate('onClick()'); // To trigger viewer.onRendered
        }, 10000);
      });
    }
    }

    return {tview, viewer};
  }

  static getOrCreate(gridCell: DG.GridCell): PdbGridCellRendererBack {
    const [gridCol, tableCol, temp] =
      getGridCellColTemp<string, PdbGridCellRendererBack>(gridCell);

    let res: PdbGridCellRendererBack = temp.rendererBack;
    if (!res) res = temp.rendererBack = new PdbGridCellRendererBack(gridCol, tableCol);
    return res;
  }

  // -- IPdbGridCellRenderer --

  private _onClicked: Subject<void> = new Subject<void>();
  get onClicked(): Observable<void> { return this._onClicked; }
}

export class PdbGridCellRenderer extends DG.GridCellRenderer {
  get name(): string { return 'Molecule3D'; }

  get cellType(): string { return 'Molecule3D'; }

  get defaultHeight(): number { return 300; }

  get defaultWidth(): number { return 300; }

  // eslint-disable-next-line max-len
  // static imgDG = await base64ToImg('data:image/png;base64, ');

  onClick(gridCell: DG.GridCell, e: MouseEvent): void {
    // Disabled this code as it doesn't align with the intended behavior.
    /*_package.logger.debug('BsV: PdbGridCellRenderer.onClick()');
    const back = PdbGridCellRendererBack.getOrCreate(gridCell);
    back.onClick(gridCell, e);*/
  }

  /** Escape on a Mol3D cell clears picker highlights on the linked SMILES column.
   *  Keydowns go to the focused cell's renderer at Dart level; the Chem
   *  renderer's onKeyDown and grid-root listener don't fire in this case. */
  override onKeyDown(gridCell: DG.GridCell, e: KeyboardEvent): void {
    if (e.key !== 'Escape') return;
    const mol3DCol = gridCell.tableColumn;
    const df = mol3DCol?.dataFrame;
    if (!mol3DCol || !df) return;
    const smilesColName = findLinkedSmilesColName(df, mol3DCol.name);
    const smilesCol = smilesColName ? df.col(smilesColName) : null;
    if (smilesCol) clearAtomPickerHighlights(smilesCol);
  }

  /**
   * Cell renderer function.
   *
   * @param {CanvasRenderingContext2D} g Canvas rendering context.
   * @param {number} x x coordinate on the canvas.
   * @param {number} y y coordinate on the canvas.
   * @param {number} w width of the cell.
   * @param {number} h height of the cell.
   * @param {DG.GridCell} gridCell Grid cell.
   * @param {DG.GridCellStyle} cellStyle Cell style.
   */
  render(
    g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
    gridCell: DG.GridCell, cellStyle: DG.GridCellStyle,
  ): void {
    if (!gridCell.cell.column)
      return;
    const back = PdbGridCellRendererBack.getOrCreate(gridCell);
    back.render(g, x, y, w, h, gridCell, cellStyle);
  }
}

/// Shows PDB id when the cell is small, and renders protein if the cell is higher than 40 pixels
export class PdbIdGridCellRenderer extends DG.GridCellRenderer {
  imageCache: LruCache<string, HTMLImageElement> = new LruCache<string, HTMLImageElement>();
  pdbIdHeight: number = 25;

  get defaultWidth() { return 100; }
  get defaultHeight() { return 100; }
  get name(): string { return 'PDB_ID'; }
  get cellType(): string { return 'PDB_ID'; }

  render(
    g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
    gridCell: DG.GridCell, cellStyle: DG.GridCellStyle,
  ): void {
    const renderedOnGrid = gridCell.gridColumn && gridCell.grid?.canvas === g.canvas;
    const cellValue = (renderedOnGrid ? gridCell.cell.valueString : (gridCell.cell?.value ?? '').toString()).toLowerCase();

    if (h < 40 || cellValue.length < 4 || w < 40) {
      // Small cell - render text only
      DG.GridCellRenderer.byName('string')?.render(g, x, y, w, h, gridCell, cellStyle);
    } else {
      // Large cell - render image with text header
      this.renderLargeCell(g, x, y, w, h, gridCell, cellStyle, cellValue, renderedOnGrid);
    }
  }

  private renderLargeCell(
    g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number,
    gridCell: DG.GridCell, cellStyle: DG.GridCellStyle, cellValue: string, renderedOnGrid: boolean
  ): void {
    const pdb = cellValue;
    const url = `https://cdn.rcsb.org/images/structures/${pdb}_assembly-1.jpeg`;
    const cache = this.imageCache;

    if (this.imageCache.has(url)) {
      const img = this.imageCache.get(url);
      if (img) {
        const fit = new DG.Rect(x, y + this.pdbIdHeight, w, h - this.pdbIdHeight).fit(img.width, img.height);
        g.drawImage(img, fit.x, fit.y, fit.width, fit.height);

        // Render text in header
        DG.GridCellRenderer.byName('string')?.render(g, x, y, w, this.pdbIdHeight, gridCell, cellStyle);
      }
    } else {
      // Render text in header while loading
      DG.GridCellRenderer.byName('string')?.render(g, x, y, w, this.pdbIdHeight, gridCell, cellStyle);

      fetch(url).then(async (response) => {
        if (!response.ok) {
          g.fillStyle = 'red';
          g.fillRect(x + w - 5, y, 5, 5);
          return;
        }

        const blob = await response.blob();
        const img = new Image();
        img.src = URL.createObjectURL(blob);
        img.onload = () => {
          cache.set(url, img);
          if (renderedOnGrid)
            gridCell.render();
          else
            this.render(g, x, y + this.pdbIdHeight, w, h - this.pdbIdHeight, gridCell, cellStyle);
          URL.revokeObjectURL(img.src);
        };
      }).catch((_) => { });
    }
  }
}
