/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  ContextMenuRegistry,
  ShortcutRegistry,
  isCopyable,
  Msg,
  ShortcutItems,
  WorkspaceSvg,
} from 'blockly';
import * as Constants from '../constants';
import {Navigation} from '../navigation';
import {getShortActionShortcut} from '../shortcut_formatting';
import {clearPasteHints, showCopiedHint, showCutHint} from '../hints';

/**
 * Weight for the first of these three items in the context menu.
 * Changing base weight will change where this group goes in the context
 * menu; changing individual weights relative to base weight can change
 * the order within the clipboard group.
 */
const BASE_WEIGHT = 12;

/**
 * Logic and state for cut/copy/paste actions as both keyboard shortcuts
 * and context menu items.
 * In the long term, this will likely merge with the clipboard code in core.
 */
export class Clipboard {
  /** The workspace a copy or cut keyboard shortcut happened in. */
  private copyWorkspace: WorkspaceSvg | null = null;

  private oldCutShortcut: ShortcutRegistry.KeyboardShortcut | undefined;
  private oldCopyShortcut: ShortcutRegistry.KeyboardShortcut | undefined;
  private oldPasteShortcut: ShortcutRegistry.KeyboardShortcut | undefined;

  constructor(private navigation: Navigation) {}

  /**
   * Install these actions as both keyboard shortcuts and context menu items.
   */
  install() {
    this.registerCopyShortcut();
    this.registerCopyContextMenuAction();

    this.registerPasteShortcut();
    this.registerPasteContextMenuAction();

    this.registerCutShortcut();
    this.registerCutContextMenuAction();
  }

  /**
   * Uninstall this action as both a keyboard shortcut and a context menu item.
   * N. B. This does *not* currently reinstall the original keyboard shortcuts.
   * You should manually reinstall the previously registered shortcuts (either
   * from core or from another plugin you may be using).
   */
  uninstall() {
    ContextMenuRegistry.registry.unregister('blockCutFromContextMenu');
    ContextMenuRegistry.registry.unregister('blockCopyFromContextMenu');
    ContextMenuRegistry.registry.unregister('blockPasteFromContextMenu');

    ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.CUT);
    ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.COPY);
    ShortcutRegistry.registry.unregister(Constants.SHORTCUT_NAMES.PASTE);
  }

  /**
   * Create and register the keyboard shortcut for the cut action.
   * Identical to the one in core but adds a toast after successful cut.
   */
  private registerCutShortcut() {
    this.oldCutShortcut =
      ShortcutRegistry.registry.getRegistry()[ShortcutItems.names.CUT];
    if (!this.oldCutShortcut)
      throw new Error('No cut keyboard shortcut registered initially');

    const cutShortcut: ShortcutRegistry.KeyboardShortcut = {
      name: Constants.SHORTCUT_NAMES.CUT,
      preconditionFn: this.oldCutShortcut.preconditionFn,
      callback: this.cutCallback.bind(this),
      // The registry gives back keycodes as an object instead of an array
      // See https://github.com/google/blockly/issues/9008
      keyCodes: this.oldCutShortcut.keyCodes,
      allowCollision: false,
    };

    ShortcutRegistry.registry.unregister(ShortcutItems.names.CUT);
    ShortcutRegistry.registry.register(cutShortcut);
  }

  /**
   * Register the cut block action as a context menu item.
   * The context menu uses its own preconditionFn (that doesn't check
   * if a gesture is in progress, because one always is in the context
   * menu). It calls the cut callback that is shared between keyboard
   * and context menu.
   */
  private registerCutContextMenuAction() {
    const cutAction: ContextMenuRegistry.RegistryItem = {
      displayText: (scope) =>
        Msg['CUT_SHORTCUT'].replace(
          '%1',
          getShortActionShortcut(Constants.SHORTCUT_NAMES.CUT),
        ),
      preconditionFn: (scope) => this.cutPrecondition(scope),
      callback: (scope, menuOpenEvent) => {
        if (!isCopyable(scope.focusedNode)) return false;
        const ws = scope.focusedNode.workspace;
        if (!(ws instanceof WorkspaceSvg)) return false;

        return this.cutCallback(ws, menuOpenEvent, undefined, scope);
      },
      id: 'blockCutFromContextMenu',
      weight: BASE_WEIGHT,
    };

    ContextMenuRegistry.registry.register(cutAction);
  }

  /**
   * Precondition function for the cut context menu. This wraps the core cut
   * precondition to support context menus.
   *
   * @param scope scope of the shortcut or context menu item
   * @returns 'enabled' if the node can be cut, 'disabled' otherwise.
   */
  private cutPrecondition(scope: ContextMenuRegistry.Scope): string {
    const focused = scope.focusedNode;
    if (!focused || !isCopyable(focused)) return 'hidden';

    const workspace = focused.workspace;
    if (!(workspace instanceof WorkspaceSvg)) return 'hidden';

    if (
      this.oldCutShortcut?.preconditionFn &&
      this.oldCutShortcut.preconditionFn(workspace, scope)
    ) {
      return 'enabled';
    }
    return 'disabled';
  }

  /**
   * Precondition function for the copy context menu. This wraps the core copy
   * precondition to support context menus.
   *
   * @param scope scope of the shortcut or context menu item
   * @returns 'enabled' if the node can be copied, 'disabled' otherwise.
   */
  private copyPrecondition(scope: ContextMenuRegistry.Scope): string {
    const focused = scope.focusedNode;
    if (!focused || !isCopyable(focused)) return 'hidden';

    const workspace = focused.workspace;
    if (!(workspace instanceof WorkspaceSvg)) return 'hidden';

    if (
      this.oldCopyShortcut?.preconditionFn &&
      this.oldCopyShortcut.preconditionFn(workspace, scope)
    ) {
      return 'enabled';
    }
    return 'disabled';
  }

  /**
   * Precondition function for the paste context menu. This wraps the core
   * paste precondition to support context menus.
   *
   * @param scope scope of the shortcut or context menu item
   * @returns 'enabled' if the node can be pasted, 'disabled' otherwise.
   */
  private pastePrecondition(scope: ContextMenuRegistry.Scope): string {
    if (!this.copyWorkspace) return 'disabled';

    if (
      this.oldPasteShortcut?.preconditionFn &&
      this.oldPasteShortcut.preconditionFn(this.copyWorkspace, scope)
    ) {
      return 'enabled';
    }
    return 'disabled';
  }

  /**
   * The callback for the cut action. Uses the registered version of the cut callback
   * to perform the cut logic, then pops a toast if cut happened.
   *
   * @param workspace Workspace where shortcut happened.
   * @param e menu open event or keyboard event
   * @param shortcut keyboard shortcut or undefined for context menus
   * @param scope scope of the shortcut or context menu item
   * @returns true if a cut happened, false otherwise
   */
  private cutCallback(
    workspace: WorkspaceSvg,
    e: Event,
    shortcut: ShortcutRegistry.KeyboardShortcut = {
      name: Constants.SHORTCUT_NAMES.CUT,
    },
    scope: ContextMenuRegistry.Scope,
  ) {
    const didCut =
      !!this.oldCutShortcut?.callback &&
      this.oldCutShortcut.callback(workspace, e, shortcut, scope);
    if (didCut) {
      this.copyWorkspace = workspace;
      showCutHint(workspace);
    }
    return didCut;
  }

  /**
   * Create and register the keyboard shortcut for the copy action.
   * Identical to the one in core but pops a toast after succesful copy.
   */
  private registerCopyShortcut() {
    this.oldCopyShortcut =
      ShortcutRegistry.registry.getRegistry()[ShortcutItems.names.COPY];
    if (!this.oldCopyShortcut)
      throw new Error('No copy keyboard shortcut registered initially');

    const copyShortcut: ShortcutRegistry.KeyboardShortcut = {
      name: Constants.SHORTCUT_NAMES.COPY,
      preconditionFn: this.oldCopyShortcut.preconditionFn,
      callback: this.copyCallback.bind(this),
      // The registry gives back keycodes as an object instead of an array
      // See https://github.com/google/blockly/issues/9008
      keyCodes: this.oldCopyShortcut.keyCodes,
      allowCollision: false,
    };

    ShortcutRegistry.registry.unregister(ShortcutItems.names.COPY);
    ShortcutRegistry.registry.register(copyShortcut);
  }

  /**
   * Register the copy block action as a context menu item.
   * The context menu uses its own preconditionFn (that doesn't check
   * if a gesture is in progress, because one always is in the context
   * menu). It calls the copy callback that is shared between keyboard
   * and context menu.
   */
  private registerCopyContextMenuAction() {
    const copyAction: ContextMenuRegistry.RegistryItem = {
      displayText: (scope) =>
        Msg['COPY_SHORTCUT'].replace(
          '%1',
          getShortActionShortcut(Constants.SHORTCUT_NAMES.COPY),
        ),
      preconditionFn: (scope) => this.copyPrecondition(scope),
      callback: (scope, menuOpenEvent) => {
        if (!isCopyable(scope.focusedNode)) return false;
        const ws = scope.focusedNode.workspace;
        if (!(ws instanceof WorkspaceSvg)) return false;

        return this.copyCallback(ws, menuOpenEvent, undefined, scope);
      },
      id: 'blockCopyFromContextMenu',
      weight: BASE_WEIGHT + 1,
    };

    ContextMenuRegistry.registry.register(copyAction);
  }

  /**
   * The callback for the copy action. Uses the registered version of the copy callback
   * to perform the copy logic, then pops a toast if copy happened.
   *
   * @param workspace Workspace where shortcut happened.
   * @param e menu open event or keyboard event
   * @param shortcut keyboard shortcut or undefined for context menus
   * @param scope scope of the shortcut or context menu item
   * @returns true if a copy happened, false otherwise
   */
  private copyCallback(
    workspace: WorkspaceSvg,
    e: Event,
    shortcut: ShortcutRegistry.KeyboardShortcut = {
      name: Constants.SHORTCUT_NAMES.CUT,
    },
    scope: ContextMenuRegistry.Scope,
  ) {
    const didCopy =
      !!this.oldCopyShortcut?.callback &&
      this.oldCopyShortcut.callback(workspace, e, shortcut, scope);
    if (didCopy) {
      this.copyWorkspace = workspace;
      showCopiedHint(workspace);
    }
    return didCopy;
  }

  /**
   * Create and register the keyboard shortcut for the paste action.
   * Identical to the one in core but clears any paste toasts after.
   */
  private registerPasteShortcut() {
    this.oldPasteShortcut =
      ShortcutRegistry.registry.getRegistry()[ShortcutItems.names.PASTE];
    if (!this.oldPasteShortcut)
      throw new Error('No paste keyboard shortcut registered initially');

    const pasteShortcut: ShortcutRegistry.KeyboardShortcut = {
      name: Constants.SHORTCUT_NAMES.PASTE,
      preconditionFn: this.oldPasteShortcut.preconditionFn,
      callback: this.pasteCallback.bind(this),
      // The registry gives back keycodes as an object instead of an array
      // See https://github.com/google/blockly/issues/9008
      keyCodes: this.oldPasteShortcut.keyCodes,
      allowCollision: false,
    };

    ShortcutRegistry.registry.unregister(ShortcutItems.names.PASTE);
    ShortcutRegistry.registry.register(pasteShortcut);
  }

  /**
   * Register the paste block action as a context menu item.
   * The context menu uses its own preconditionFn (that doesn't check
   * if a gesture is in progress, because one always is in the context
   * menu). It calls the paste callback that is shared between keyboard
   * and context menu.
   */
  private registerPasteContextMenuAction() {
    const pasteAction: ContextMenuRegistry.RegistryItem = {
      displayText: (scope) =>
        Msg['PASTE_SHORTCUT'].replace(
          '%1',
          getShortActionShortcut(Constants.SHORTCUT_NAMES.PASTE),
        ),
      preconditionFn: (scope) => this.pastePrecondition(scope),
      callback: (scope: ContextMenuRegistry.Scope, menuOpenEvent: Event) => {
        const workspace = this.copyWorkspace;
        if (!workspace) return;
        return this.pasteCallback(workspace, menuOpenEvent, undefined, scope);
      },
      id: 'blockPasteFromContextMenu',
      weight: BASE_WEIGHT + 2,
    };

    ContextMenuRegistry.registry.register(pasteAction);
  }

  /**
   * The callback for the paste action. Uses the registered version of the paste callback
   * to perform the paste logic, then clears any toasts about pasting.
   *
   * @param workspace Workspace where shortcut happened.
   * @param e menu open event or keyboard event
   * @param shortcut keyboard shortcut or undefined for context menus
   * @param scope scope of the shortcut or context menu item
   * @returns true if a paste happened, false otherwise
   */
  private pasteCallback(
    workspace: WorkspaceSvg,
    e: Event,
    shortcut: ShortcutRegistry.KeyboardShortcut = {
      name: Constants.SHORTCUT_NAMES.CUT,
    },
    scope: ContextMenuRegistry.Scope,
  ) {
    const didPaste =
      !!this.oldPasteShortcut?.callback &&
      this.oldPasteShortcut.callback(workspace, e, shortcut, scope);

    // Clear the paste hints regardless of whether something was pasted
    // Some implementations of paste are async and we should clear the hint
    // once the user initiates the paste action.
    clearPasteHints(workspace);
    return didPaste;
  }
}
