import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import type { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
import {
  Command,
  CommandRegistry,
  CoreServiceContribution,
  KeybindingRegistry,
  MenuModelRegistry,
  TabBarToolbarRegistry,
} from './contribution';
import { CoreErrorHandler } from './core-error-handler';

export interface VerifySketchParams {
  /**
   * Same as `CoreService.Options.Compile#exportBinaries`
   */
  readonly exportBinaries?: boolean;
  /**
   * If `true`, there won't be any UI indication of the verify command in the toolbar. It's `false` by default.
   */
  readonly silent?: boolean;
}

/**
 *  - `"idle"` when neither verify, nor upload is running,
 *  - `"explicit-verify"` when only verify is running triggered by the user, and
 *  - `"automatic-verify"` is when the automatic verify phase is running as part of an upload triggered by the user.
 */
type VerifyProgress = 'idle' | 'explicit-verify' | 'automatic-verify';

@injectable()
export class VerifySketch extends CoreServiceContribution {
  @inject(CoreErrorHandler)
  private readonly coreErrorHandler: CoreErrorHandler;

  private readonly onDidChangeEmitter = new Emitter<void>();
  private readonly onDidChange = this.onDidChangeEmitter.event;
  private verifyProgress: VerifyProgress = 'idle';

  override registerCommands(registry: CommandRegistry): void {
    registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH, {
      execute: (params?: VerifySketchParams) => this.verifySketch(params),
      isEnabled: () => this.verifyProgress === 'idle',
    });
    registry.registerCommand(VerifySketch.Commands.EXPORT_BINARIES, {
      execute: () => this.verifySketch({ exportBinaries: true }),
      isEnabled: () => this.verifyProgress === 'idle',
    });
    registry.registerCommand(VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR, {
      isVisible: (widget) =>
        ArduinoToolbar.is(widget) && widget.side === 'left',
      isEnabled: () => this.verifyProgress !== 'explicit-verify',
      // toggled only when verify is running, but not toggled when automatic verify is running before the upload
      // https://github.com/arduino/arduino-ide/pull/1750#pullrequestreview-1214762975
      isToggled: () => this.verifyProgress === 'explicit-verify',
      execute: () =>
        registry.executeCommand(VerifySketch.Commands.VERIFY_SKETCH.id),
    });
  }

  override registerMenus(registry: MenuModelRegistry): void {
    registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
      commandId: VerifySketch.Commands.VERIFY_SKETCH.id,
      label: nls.localize('arduino/sketch/verifyOrCompile', 'Verify/Compile'),
      order: '0',
    });
    registry.registerMenuAction(ArduinoMenus.SKETCH__MAIN_GROUP, {
      commandId: VerifySketch.Commands.EXPORT_BINARIES.id,
      label: nls.localize(
        'arduino/sketch/exportBinary',
        'Export Compiled Binary'
      ),
      order: '4',
    });
  }

  override registerKeybindings(registry: KeybindingRegistry): void {
    registry.registerKeybinding({
      command: VerifySketch.Commands.VERIFY_SKETCH.id,
      keybinding: 'CtrlCmd+R',
    });
    registry.registerKeybinding({
      command: VerifySketch.Commands.EXPORT_BINARIES.id,
      keybinding: 'CtrlCmd+Alt+S',
    });
  }

  override registerToolbarItems(registry: TabBarToolbarRegistry): void {
    registry.registerItem({
      id: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
      command: VerifySketch.Commands.VERIFY_SKETCH_TOOLBAR.id,
      tooltip: nls.localize('arduino/sketch/verify', 'Verify'),
      priority: 0,
      onDidChange: this.onDidChange,
    });
  }

  protected override handleError(error: unknown): void {
    this.coreErrorHandler.tryHandle(error);
    super.handleError(error);
  }

  private async verifySketch(
    params?: VerifySketchParams
  ): Promise<CoreService.Options.Compile | undefined> {
    if (this.verifyProgress !== 'idle') {
      return undefined;
    }

    try {
      this.verifyProgress = params?.silent
        ? 'automatic-verify'
        : 'explicit-verify';
      this.onDidChangeEmitter.fire();
      this.menuManager.update();
      this.clearVisibleNotification();
      this.coreErrorHandler.reset();

      const options = await this.options(params?.exportBinaries);
      if (!options) {
        return undefined;
      }

      await this.doWithProgress({
        progressText: nls.localize(
          'arduino/sketch/compile',
          'Compiling sketch...'
        ),
        task: (progressId, coreService, token) =>
          coreService.compile(
            {
              ...options,
              progressId,
            },
            token
          ),
        cancelable: true,
      });
      this.messageService.info(
        nls.localize('arduino/sketch/doneCompiling', 'Done compiling.'),
        { timeout: 3000 }
      );
      // Returns with the used options for the compilation
      // so that follow-up tasks (such as upload) can reuse the compiled code.
      // Note that the `fqbn` is already decorated with the board settings, if any.
      return options;
    } catch (e) {
      this.handleError(e);
      return undefined;
    } finally {
      this.verifyProgress = 'idle';
      this.onDidChangeEmitter.fire();
      this.menuManager.update();
    }
  }

  private async options(
    exportBinaries?: boolean
  ): Promise<CoreService.Options.Compile | undefined> {
    const sketch = await this.sketchServiceClient.currentSketch();
    if (!CurrentSketch.isValid(sketch)) {
      return undefined;
    }
    const { boardsConfig } = this.boardsServiceProvider;
    const [fqbn, sourceOverride, optimizeForDebug] = await Promise.all([
      this.boardsDataStore.appendConfigToFqbn(boardsConfig.selectedBoard?.fqbn),
      this.sourceOverride(),
      this.commandService.executeCommand<boolean>(
        'arduino-is-optimize-for-debug'
      ),
    ]);
    const verbose = this.preferences.get('arduino.compile.verbose');
    const compilerWarnings = this.preferences.get('arduino.compile.warnings');
    return {
      sketch,
      fqbn,
      optimizeForDebug: Boolean(optimizeForDebug),
      verbose,
      exportBinaries,
      sourceOverride,
      compilerWarnings,
    };
  }
}

export namespace VerifySketch {
  export namespace Commands {
    export const VERIFY_SKETCH: Command = {
      id: 'arduino-verify-sketch',
    };
    export const EXPORT_BINARIES: Command = {
      id: 'arduino-export-binaries',
    };
    export const VERIFY_SKETCH_TOOLBAR: Command = {
      id: 'arduino-verify-sketch--toolbar',
    };
  }
}