import { CommandSquisher } from "./collaborative/command_squisher";
import { LocalTransportService } from "./collaborative/local_transport_service";
import { ReadonlyTransportFilter } from "./collaborative/readonly_transport_filter";
import { Session } from "./collaborative/session";
import { DEFAULT_REVISION_ID } from "./constants";
import { EventBus } from "./helpers/event_bus";
import { deepCopy, deepEquals, lazy } from "./helpers/misc";
import { UuidGenerator } from "./helpers/uuid";
import { buildRevisionLog } from "./history/factory";
import {
  createEmptyExcelWorkbookData,
  createEmptyWorkbookData,
  load,
  repairInitialMessages,
} from "./migrations/data";
import { BasePlugin } from "./plugins/base_plugin";
import { FormulaProviderAggregator } from "./plugins/core/formulas_provider";
import { RangeAdapterPlugin } from "./plugins/core/range";
import { CorePlugin, CorePluginConfig, CorePluginConstructor } from "./plugins/core_plugin";
import {
  EvaluationPlugin,
  EvaluationPluginConfig,
  EvaluationPluginConstructor,
} from "./plugins/evaluation_plugin";
import {
  corePluginRegistry,
  evaluationPluginRegistry,
  featurePluginRegistry,
  statefulUIPluginRegistry,
} from "./plugins/plugin_registries";
import { UIPlugin, UIPluginConfig, UIPluginConstructor } from "./plugins/ui_plugin";
import { SelectionStreamProcessorImpl } from "./selection_stream/selection_stream_processor";
import { StateObserver } from "./state_observer";
import { _t, setDefaultTranslationMethod } from "./translation";
import { StateUpdateMessage } from "./types/collaborative/transport_service";
import {
  canExecuteInReadonly,
  Command,
  CommandDispatcher,
  CommandHandler,
  CommandResult,
  CommandTypes,
  CoreCommand,
  DispatchResult,
  EvaluationCommandDispatcher,
  isCoreCommand,
  isDispatcheableEvaluationCommand,
  isEvaluationCommand,
} from "./types/commands";
import { CoreGetters, EvaluationGetters, Getters } from "./types/getters";
import { DEFAULT_LOCALES } from "./types/locale";
import { UID } from "./types/misc";
import { Mode, ModelConfig, ModelExternalConfig } from "./types/model";
import { GridRenderingContext, LayerName } from "./types/rendering";
import { SelectionStreamProcessor } from "./types/selection_stream_processor";
import { WorkbookData } from "./types/workbook_data";
import { XLSXExport } from "./types/xlsx";
import { getXLSX } from "./xlsx/xlsx_writer";

const enum Status {
  Ready,
  Running,
  RunningCore,
  RunningEvaluation,
  Finalizing,
}

/**
 * Model
 *
 * The Model class is the owner of the state of the Spreadsheet. However, it
 * has more a coordination role: it defers the actual state manipulation work to
 * plugins.
 *
 * At creation, the Model instantiates all necessary plugins. They each have
 * a private state (for example, the Selection plugin has the current selection).
 *
 * State changes are then performed through commands.  Commands are dispatched
 * to the model, which will then relay them to each plugins (and the history
 * handler). Then, the model will trigger an 'update' event to notify whoever
 * is concerned that the command was applied (if it was not canceled).
 *
 * Also, the model has an unconventional responsibility: it actually renders the
 * visible viewport on a canvas. This is because each plugins actually manage a
 * specific concern about the content of the spreadsheet, and it is more natural
 * if they are able to read data from their internal state to represent it on the
 * screen.
 *
 * Note that the Model can be used in a standalone way to manipulate
 * programmatically a spreadsheet.
 */
export class Model extends EventBus<any> implements CommandDispatcher {
  private statefulUIPlugins: UIPlugin[] = [];

  private range: RangeAdapterPlugin;
  private formulasPlugin: FormulaProviderAggregator;

  private session: Session;

  /**
   * In a collaborative context, some commands can be replayed, we have to ensure
   * that these commands are not replayed on the UI plugins.
   */
  private isReplayingCommand: boolean = false;

  /**
   * A plugin can draw some contents on the canvas. But even better: it can do
   * so multiple times.  The order of the render calls will determine a list of
   * "layers" (i.e., earlier calls will be obviously drawn below later calls).
   * This list simply keeps the renderers+layer information so the drawing code
   * can just iterate on it
   */
  private renderers: Partial<Record<LayerName, UIPlugin[]>> = {};

  /**
   * Internal status of the model. Important for command handling coordination
   */
  private status: Status = Status.Ready;

  /**
   * The config object contains some configuration flag and callbacks
   */
  readonly config: ModelConfig;
  private corePluginConfig: CorePluginConfig;
  private evaluationPluginConfig: EvaluationPluginConfig;
  private uiPluginConfig: UIPluginConfig;

  private state: StateObserver;

  readonly selection: SelectionStreamProcessor;

  /**
   * Getters are the main way the rest of the UI read data from the model. Also,
   * it is shared between all plugins, so they can also communicate with each
   * other.
   */
  getters: Getters;

  /**
   * Getters that are accessible from the core plugins. It's a subset of `getters`,
   * without the UI getters and without the evaluation getters
   */
  private coreGetters: CoreGetters;

  /**
   * Getters that are accessible from the evaluation plugins. It's a subset of
   * `getters`, without the UI getters.
   */
  private evaluationGetters: EvaluationGetters;

  private readonly handlers: CommandHandler<Command>[] = [];
  private readonly uiHandlers: CommandHandler<Command>[] = [];
  private readonly coreHandlers: CommandHandler<CoreCommand>[] = [];
  private readonly evaluationHandlers: CommandHandler<Command>[] = [];

  constructor(
    data: any = {},
    config: Partial<ModelConfig> = {},
    stateUpdateMessages: StateUpdateMessage[] = [],
    verboseImport = false
  ) {
    const start = performance.now();
    console.debug("##### Model creation #####");
    super();
    setDefaultTranslationMethod();

    stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);

    const workbookData = load(data, verboseImport);

    this.state = new StateObserver();

    this.config = this.setupConfig(config);

    this.getters = {
      isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
      isDashboard: () => this.config.mode === "dashboard",
    } as Getters;
    this.evaluationGetters = {
      isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
      isDashboard: () => this.config.mode === "dashboard",
    } as EvaluationGetters;
    this.coreGetters = {} as CoreGetters;

    this.session = this.setupSession(workbookData.revisionId);

    this.range = new RangeAdapterPlugin(this.coreGetters);
    for (const name of RangeAdapterPlugin.getters) {
      this.registerCoreGetter(this.range, name);
    }
    this.formulasPlugin = new FormulaProviderAggregator();
    for (const name of FormulaProviderAggregator.getters) {
      this.registerCoreGetter(this.formulasPlugin, name);
    }

    // Initiate stream processor
    this.selection = new SelectionStreamProcessorImpl(this.getters);

    this.coreHandlers.push(this.range);
    this.handlers.push(this.range);

    this.corePluginConfig = this.getCorePluginConfig();
    this.evaluationPluginConfig = this.getEvaluationPluginConfig();
    this.uiPluginConfig = this.getUiPluginConfig();

    // registering plugins
    for (const Plugin of corePluginRegistry.getAll()) {
      this.setupCorePlugin(Plugin, workbookData);
    }

    this.session.loadInitialMessages(stateUpdateMessages);

    for (const Plugin of evaluationPluginRegistry.getAll()) {
      const plugin = this.setupEvaluationPlugin(Plugin);
      this.handlers.push(plugin);
      this.uiHandlers.push(plugin);
      this.coreHandlers.push(plugin);
      this.evaluationHandlers.push(plugin);
    }
    for (const Plugin of statefulUIPluginRegistry.getAll()) {
      const plugin = this.setupUiPlugin(Plugin);
      this.statefulUIPlugins.push(plugin);
      this.handlers.push(plugin);
      this.uiHandlers.push(plugin);
      this.evaluationHandlers.push(plugin);
    }
    for (const Plugin of featurePluginRegistry.getAll()) {
      const plugin = this.setupUiPlugin(Plugin);
      this.handlers.push(plugin);
      this.uiHandlers.push(plugin);
      this.evaluationHandlers.push(plugin);
    }

    if (this.config.mode !== "export_verification") {
      // starting plugins
      this.dispatch("START");
      // Model should be the last permanent subscriber in the list since he should render
      // after all changes have been applied to the other subscribers (plugins)
      this.selection.observe(this, {
        handleEvent: () => this.trigger("update"),
      });
      // This should be done after construction of LocalHistory due to order of
      // events
      this.setupSessionEvents();

      this.joinSession();

      if (config.snapshotRequested || (data["[Content_Types].xml"] && !this.getters.isReadonly())) {
        const startSnapshot = performance.now();
        console.debug("Snapshot requested");
        // We do not need to wait for the snapshot to be sent
        void this.session.snapshot(this.exportData());
        console.debug("Snapshot taken in", performance.now() - startSnapshot, "ms");
      }
    }
    console.debug("Model created in", performance.now() - start, "ms");
    console.debug("######");
  }

  joinSession() {
    this.session.join(this.config.client);
  }

  async leaveSession({ shouldSnapshot }: { shouldSnapshot?: boolean } = { shouldSnapshot: true }) {
    const snapshot =
      shouldSnapshot && !this.getters.isReadonly() ? lazy(() => this.exportData()) : undefined;
    await this.session.leave(snapshot);
  }

  private registerCoreGetter(
    plugin: CorePlugin | RangeAdapterPlugin | FormulaProviderAggregator,
    name: string
  ) {
    if (!(name in plugin)) {
      throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);
    }
    if (name in this.coreGetters) {
      throw new Error(`Getter "${name}" is already defined.`);
    }
    this.coreGetters[name] = plugin[name].bind(plugin);
    this.evaluationGetters[name] = plugin[name].bind(plugin);
    this.getters[name] = plugin[name].bind(plugin);
  }

  private registerEvaluationGetter(plugin: EvaluationPlugin, name: string) {
    if (!(name in plugin)) {
      throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);
    }
    if (name in this.evaluationGetters) {
      throw new Error(`Getter "${name}" is already defined.`);
    }
    this.evaluationGetters[name] = plugin[name].bind(plugin);
    this.getters[name] = plugin[name].bind(plugin);
  }

  private registerUiGetter(plugin: UIPlugin, name: string) {
    if (!(name in plugin)) {
      throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`);
    }
    if (name in this.getters) {
      throw new Error(`Getter "${name}" is already defined.`);
    }
    this.getters[name] = plugin[name].bind(plugin);
  }

  private setupUiPlugin(Plugin: UIPluginConstructor) {
    const plugin = new Plugin(this.uiPluginConfig);
    for (const name of Plugin.getters) {
      this.registerUiGetter(plugin, name);
    }
    for (const layer of Plugin.layers) {
      if (!this.renderers[layer]) {
        this.renderers[layer] = [];
      }
      this.renderers[layer]!.push(plugin);
    }
    return plugin;
  }

  private setupEvaluationPlugin(Plugin: EvaluationPluginConstructor) {
    const plugin = new Plugin(this.evaluationPluginConfig);
    for (const name of Plugin.getters) {
      this.registerEvaluationGetter(plugin, name);
    }
    return plugin;
  }

  /**
   * Initialize and properly configure a plugin.
   */
  private setupCorePlugin(Plugin: CorePluginConstructor, data: WorkbookData) {
    const plugin = new Plugin(this.corePluginConfig);
    for (const name of Plugin.getters) {
      this.registerCoreGetter(plugin, name);
    }
    plugin.import(data);
    this.coreHandlers.push(plugin);
    this.handlers.push(plugin);
  }

  private onRemoteRevisionReceived({ commands }: { commands: readonly CoreCommand[] }) {
    for (const command of commands) {
      const previousStatus = this.status;
      this.status = Status.RunningCore;
      this.dispatchToHandlers(this.statefulUIPlugins, command);
      this.status = previousStatus;
    }
    this.finalize();
  }

  private setupSession(revisionId: UID): Session {
    return new Session(
      buildRevisionLog({
        initialRevisionId: revisionId,
        recordChanges: this.state.recordChanges.bind(this.state),
        dispatch: (command: CoreCommand) => {
          const result = this.checkDispatchAllowedRemoteCommand(command);
          if (!result.isSuccessful) {
            // evaluation plugins need to be invalidated
            this.dispatchToHandlers(this.coreHandlers, {
              type: "UNDO",
              commands: [command],
            });
            return;
          }
          this.isReplayingCommand = true;
          this.dispatchToHandlers(this.coreHandlers, command);
          this.isReplayingCommand = false;
        },
      }),
      this.config.transportService,
      revisionId,
      new CommandSquisher(this.getters)
    );
  }

  private setupSessionEvents() {
    this.session.on("remote-revision-received", this, this.onRemoteRevisionReceived);
    this.session.on("revision-undone", this, ({ commands }) => {
      this.dispatchFromCorePlugin("UNDO", { commands });
      this.finalize();
    });
    this.session.on("revision-redone", this, ({ commands }) => {
      this.dispatchFromCorePlugin("REDO", { commands });
      this.finalize();
    });
    // How could we improve communication between the session and UI?
    // It feels weird to have the model piping specific session events to its own bus.
    this.session.on("unexpected-revision-id", this, () => this.trigger("unexpected-revision-id"));
    this.session.on("collaborative-event-received", this, () => {
      this.trigger("update");
    });
  }

  private setupConfig(config: Partial<ModelConfig>): ModelConfig {
    const client = config.client || {
      id: UuidGenerator.smallUuid(),
      name: _t("Anonymous").toString(),
    };
    const transportService = config.transportService || new LocalTransportService();
    const isReadonly =
      config.mode === "readonly" ||
      config.mode === "dashboard" ||
      config.mode === "export_verification";
    return {
      ...config,
      mode: config.mode || "normal",
      custom: config.custom || {},
      external: this.setupExternalConfig(config.external || {}),
      transportService: isReadonly
        ? new ReadonlyTransportFilter(transportService)
        : transportService,
      client,
      moveClient: () => {},
      snapshotRequested: false,
      customColors: config.customColors || [],
    };
  }

  private setupExternalConfig(external: Partial<ModelExternalConfig>): ModelExternalConfig {
    const loadLocales = external.loadLocales || (() => Promise.resolve(DEFAULT_LOCALES));
    return {
      ...external,
      loadLocales,
    };
  }

  private getCorePluginConfig(): CorePluginConfig {
    return {
      getters: this.coreGetters,
      stateObserver: this.state,
      range: this.range,
      formulasPlugin: this.formulasPlugin,
      dispatch: this.dispatchFromCorePlugin,
      canDispatch: this.canDispatch,
      custom: this.config.custom,
      external: this.config.external,
    };
  }

  private getEvaluationPluginConfig(): EvaluationPluginConfig {
    return {
      getters: this.evaluationGetters,
      stateObserver: this.state,
      custom: this.config.custom,
      session: this.session,
      dispatch: this.dispatchFromEvaluationPlugin,
      defaultCurrency: this.config.defaultCurrency,
      customColors: this.config.customColors || [],
      external: this.config.external,
    };
  }

  private getUiPluginConfig(): UIPluginConfig {
    return {
      getters: this.getters,
      stateObserver: this.state,
      dispatch: this.dispatch,
      canDispatch: this.canDispatch,
      selection: this.selection,
      moveClient: this.session.move.bind(this.session),
      custom: this.config.custom,
      session: this.session,
      defaultCurrency: this.config.defaultCurrency,
      customColors: this.config.customColors || [],
      external: this.config.external,
      colorScheme: this.config.colorScheme,
    };
  }

  // ---------------------------------------------------------------------------
  // Command Handling
  // ---------------------------------------------------------------------------

  /**
   * Check if the given command is allowed by all the plugins and the history.
   */
  private checkDispatchAllowed(command: Command): DispatchResult {
    const results = isCoreCommand(command)
      ? this.checkDispatchAllowedCoreCommand(command)
      : this.checkDispatchAllowedLocalCommand(command);
    return this.processCommandResults(results);
  }

  private processCommandResults(results: (CommandResult | CommandResult[])[]): DispatchResult {
    if (results.some((r) => r !== CommandResult.Success)) {
      return new DispatchResult(results.flat());
    }
    return DispatchResult.Success;
  }

  private checkDispatchAllowedRemoteCommand(command: CoreCommand): DispatchResult {
    const results = this.coreHandlers.map((handler) => handler.allowDispatch(command));
    return this.processCommandResults(results);
  }

  private checkDispatchAllowedCoreCommand(command: CoreCommand) {
    return this.handlers.map((handler) => handler.allowDispatch(command));
  }

  private checkDispatchAllowedLocalCommand(command: Command) {
    return this.uiHandlers
      .filter((handler) => this.canHandle(handler, command))
      .map((handler) => handler.allowDispatch(command));
  }

  private canHandle(handler: CommandHandler<Command>, command: Command): boolean {
    if (handler instanceof CorePlugin) {
      return isCoreCommand(command);
    }
    if (handler instanceof EvaluationPlugin) {
      return isEvaluationCommand(command);
    }
    return true;
  }

  private finalize() {
    this.status = Status.Finalizing;
    for (const h of this.handlers) {
      h.finalize();
    }
    this.status = Status.Ready;
    this.trigger("command-finalized");
  }

  /**
   * Check if a command can be dispatched, and returns a DispatchResult object with the possible
   * reasons the dispatch failed.
   */
  canDispatch: CommandDispatcher["dispatch"] = (type: string, payload?: any) => {
    return this.checkDispatchAllowed(createCommand(type, payload));
  };

  /**
   * The dispatch method is the only entry point to manipulate data in the model.
   * This is through this method that commands are dispatched most of the time
   * recursively until no plugin want to react anymore.
   *
   * CoreCommands dispatched from this function are saved in the history.
   *
   * Small technical detail: it is defined as an arrow function.  There are two
   * reasons for this:
   * 1. this means that the dispatch method can be "detached" from the model,
   *    which is done when it is put in the environment (see the Spreadsheet
   *    component)
   * 2. This allows us to define its type by using the interface CommandDispatcher
   */
  dispatch: CommandDispatcher["dispatch"] = (type: CommandTypes, payload?: any) => {
    const command: Command = createCommand(type, payload);
    const status: Status = this.status;
    if (this.getters.isReadonly() && !canExecuteInReadonly(command)) {
      return new DispatchResult(CommandResult.Readonly);
    }
    if (!this.session.canApplyOptimisticUpdate()) {
      return new DispatchResult(CommandResult.WaitingSessionConfirmation);
    }
    switch (status) {
      case Status.Ready:
        if (isDispatcheableEvaluationCommand(command)) {
          this.status = Status.RunningEvaluation;
          const start = performance.now();
          this.dispatchToHandlers(this.handlers, command);
          this.finalize();
          const time = performance.now() - start;
          if (time > 5) {
            console.debug(type, time, "ms");
          }
          this.status = Status.Ready;
          this.trigger("update");
          break;
        }
        const result = this.checkDispatchAllowed(command);
        if (!result.isSuccessful) {
          this.trigger("update");
          this.trigger("command-rejected", { command, result });
          return result;
        }
        this.status = Status.Running;
        const { changes, commands } = this.state.recordChanges(() => {
          const start = performance.now();
          if (isCoreCommand(command)) {
            this.state.addCommand(command);
          }
          this.dispatchToHandlers(this.handlers, command);
          this.finalize();
          const time = performance.now() - start;
          if (time > 5) {
            console.debug(type, time, "ms");
          }
        });
        this.session.save(command, commands, changes);
        this.status = Status.Ready;
        this.trigger("update");
        break;
      case Status.Running:
        if (isCoreCommand(command)) {
          const dispatchResult = this.checkDispatchAllowed(command);
          if (!dispatchResult.isSuccessful) {
            return dispatchResult;
          }
          this.state.addCommand(command);
        }
        this.dispatchToHandlers(this.handlers, command);
        break;
      case Status.Finalizing:
        throw new Error("Cannot dispatch commands in the finalize state");
      case Status.RunningCore:
        if (isCoreCommand(command)) {
          throw new Error(`A UI plugin cannot dispatch ${type} while handling a core command`);
        }
        this.dispatchToHandlers(this.handlers, command);
        break;
      case Status.RunningEvaluation:
        if (!isDispatcheableEvaluationCommand(command)) {
          throw new Error(
            `A top level evaluation command cannot dispatch non-evaluation commands (${type})`
          );
        }
        break;
    }
    return DispatchResult.Success;
  };

  /**
   * Dispatch a command from a Core Plugin (or the History).
   * A command dispatched from this function is not added to the history.
   */
  private dispatchFromCorePlugin: CommandDispatcher["dispatch"] = (type: string, payload?: any) => {
    const command = createCommand(type, payload);
    const previousStatus = this.status;
    this.status = Status.RunningCore;
    const handlers = this.isReplayingCommand ? this.coreHandlers : this.handlers;
    this.dispatchToHandlers(handlers, command);
    this.status = previousStatus;
    return DispatchResult.Success;
  };

  private dispatchFromEvaluationPlugin: EvaluationCommandDispatcher["dispatch"] = (
    type: string,
    payload?: any
  ) => {
    const command = createCommand(type, payload);
    if (!isDispatcheableEvaluationCommand(command)) {
      throw new Error(`An evaluation plugin cannot dispatch non-evaluation commands (${type})`);
    }
    if (this.status !== Status.Ready) {
      this.dispatchToHandlers(this.evaluationHandlers, command);
      return DispatchResult.Success;
    }
    this.status = Status.RunningEvaluation;
    this.dispatchToHandlers(this.evaluationHandlers, command);
    this.finalize();
    this.status = Status.Ready;
    this.trigger("update");
    return DispatchResult.Success;
  };

  /**
   * Dispatch the given command to the given handlers.
   * It will call `beforeHandle` and `handle`
   */
  private dispatchToHandlers(handlers: CommandHandler<Command>[], command: Command) {
    const concernedHandlers = handlers.filter((handler) => this.canHandle(handler, command));
    for (const handler of concernedHandlers) {
      handler.beforeHandle(command);
    }
    for (const handler of concernedHandlers) {
      handler.handle(command);
    }
    this.trigger("command-dispatched", command);
  }

  // ---------------------------------------------------------------------------
  // Grid Rendering
  // ---------------------------------------------------------------------------

  /**
   * When the Grid component is ready (= mounted), it has a reference to its
   * canvas and need to draw the grid on it.  This is then done by calling this
   * method, which will dispatch the call to all registered plugins.
   *
   * Note that nothing prevent multiple grid components from calling this method
   * each, or one grid component calling it multiple times with a different
   * context. This is probably the way we should do if we want to be able to
   * freeze a part of the grid (so, we would need to render different zones)
   */
  drawLayer(context: GridRenderingContext, layer: LayerName) {
    const renderers = this.renderers[layer];
    if (!renderers) {
      return;
    }
    for (const renderer of renderers) {
      context.ctx.save();
      renderer.drawLayer(context, layer);
      context.ctx.restore();
    }
  }

  // ---------------------------------------------------------------------------
  // Data Export
  // ---------------------------------------------------------------------------

  /**
   * As the name of this method strongly implies, it is useful when we need to
   * export data out of the model.
   */
  exportData(): WorkbookData {
    const squished = true;
    const unsquished = false;
    const exportSquished = this._exportData(squished);
    const exportUnsquished = this._exportData(unsquished);
    const verificationConfig = {
      ...this.config,
      mode: "export_verification" as Mode, // will not trigger evaluation or join the session
      client: { id: "exporter", name: "exporter" },
      snapshotRequested: false, // guarantee that no extra snapshot is requested
      transportService: new LocalTransportService(),
    };

    const exportVerificationModel = new Model(
      deepCopy(exportSquished), // the import itself modifies the data, so we need to deep copy it to keep the original one for comparison
      verificationConfig
    )._exportData(unsquished);

    if (!deepEquals(exportUnsquished, exportVerificationModel)) {
      exportUnsquished.isNotSquishable = true;
      return exportUnsquished;
    } else {
      return exportSquished;
    }
  }

  _exportData(shouldSquish: boolean): WorkbookData {
    let data = createEmptyWorkbookData();
    for (const handler of this.handlers) {
      if (handler instanceof CorePlugin) {
        handler.export(data, shouldSquish);
      }
    }
    data.revisionId = this.session.getRevisionId() || DEFAULT_REVISION_ID;
    data = deepCopy(data);
    return data;
  }

  updateMode(mode: Mode) {
    // @ts-ignore For testing purposes only
    this.config.mode = mode;
    this.trigger("update");
  }

  /**
   * Exports the current model data into a list of serialized XML files
   * to be zipped together as an *.xlsx file.
   *
   * We need to trigger a cell revaluation  on every sheet and ensure that even
   * async functions are evaluated.
   * This prove to be necessary if the client did not trigger that evaluation in the first place
   * (e.g. open a document with several sheet and click on download before visiting each sheet)
   */
  async exportXLSX(): Promise<XLSXExport> {
    this.dispatch("EVALUATE_CELLS");
    let data = createEmptyExcelWorkbookData();
    for (const handler of this.handlers) {
      if (handler instanceof BasePlugin) {
        await handler.exportForExcel(data);
      }
    }
    data = deepCopy(data);

    return getXLSX(data);
  }
}

function createCommand(type: string, payload: any = {}): Command {
  const command = deepCopy(payload);
  command.type = type;
  return command;
}
