<template>
  <span data-test="body-container">
    <tool-bar
      v-if="showComponent"
      ref="tool-bar"
      v-show="showToolbar"
      :canvas-drag-position="canvasDragPosition"
      :cursor="cursor"
      :is-rendering="isRendering"
      :paper-manager="paperManager"
      :breadcrumb-data="breadcrumbData"
      :panelsCompressed="panelsCompressed"
      :validation-errors="validationErrors"
      :warnings="allWarnings"
      :xml-manager="xmlManager"
      :validationBar="validationBar"
      :extra-actions="extraActions"
      @load-xml="loadXML"
      @toggle-panels-compressed="panelsCompressed = !panelsCompressed"
      @toggle-mini-map-open="miniMapOpen = $event"
      @saveBpmn="saveBpmn"
      @publishTemplate="publishTemplate"
      @publishPmBlock="publishPmBlock"
      @close="close"
      @save-state="pushToUndoStack"
      @clearSelection="clearSelection"
      :players="filteredPlayers"
      @action="handleToolbarAction"
      @onGenerateAssets="onGenerateAssets()"
    />
    <b-row class="modeler h-100">
      <b-tooltip
        v-if="tooltipTarget"
        :key="tooltipTarget.id"
        :target="getTooltipTarget"
        :title="tooltipTitle"
      />
      <explorer-rail
        v-if="showComponent"
        :node-types="nodeTypes"
        :pm-block-nodes="pmBlockNodes"
        @set-cursor="cursor = $event"
        @onCreateElement="onCreateElementHandler"
      />
      <b-col
        class="paper-container h-100 pr-4"
        ref="paper-container"
        :class="[cursor, { 'grabbing-cursor' : panMode && panning, 'grab-cursor' : panMode && !panning }]"
        :style="{ width: parentWidth, height: parentHeight }"
        @mouseup="onMouseUp"
        @mousemove="[onMouseMove($event), setInspectorButtonPosition($event)]"
      >

        <div ref="paper" data-test="paper" class="main-paper" />
      </b-col>

      <WelcomeMessage
        v-if="showWelcomeMessage"
      />

      <CreateAssetsCard
        ref="createAssetsCard"
        v-if="isAiGenerated"
        @onGenerateAssets="onGenerateAssets()"
        @closeCreateAssets="isAiGenerated = false"
      />

      <GeneratingAssetsCard
        ref="generatingAssetsCard"
        v-if="loadingAI"
        @stopAssetGeneration="onStopAssetGeneration()"
      />

      <AssetsCreatedCard
        ref="assetsCreatedCard"
        v-if="assetsCreated"
        @closeAssetsCreated="assetsCreated = false"
      />

      <CreateAssetsFailCard
        ref="createAssetsFailCard"
        v-if="assetFail"
        @onDismiss="assetFail = false"
      />

      <InspectorButton
        ref="inspector-button"
        v-show="showComponent && showInspectorButton"
        :showInspector="isOpenInspector"
        @toggleInspector="[handleToggleInspector($event), setInspectorButtonPosition($event)]"
        :style="{ right: inspectorButtonRight + 'px', top: inspectorButtonTopHeight + 'px' }"
      />

      <PreviewPanel ref="preview-panel"
        @togglePreview="[handleTogglePreview($event), setInspectorButtonPosition($event)]"
        @previewResize="[onMouseMove($event), setInspectorButtonPosition($event)]"
        @startResize="onStartPreviewResize"
        @stopResize="onMouseUp"
        :visible="isOpenPreview && !(highlightedNodes.length > 1)"
        :nodeRegistry="nodeRegistry"
        :previewConfigs="previewConfigs"
        :panelWidth="previewPanelWidth"
      />

      <NodeDocumentation ref="nodeDocumentation"/>

      <InspectorPanel
        v-if="showComponent"
        ref="inspector-panel"
        v-show="isOpenInspector && !(highlightedNodes.length > 1)"
        :style="{ height: parentHeight }"
        :nodeRegistry="nodeRegistry"
        :moddle="moddle"
        :definitions="definitions"
        :processNode="processNode"
        @save-state="pushToUndoStack"
        class="inspector h-100"
        :parent-height="parentHeight"
        :canvas-drag-position="canvasDragPosition"
        @shape-resize="shapeResize(false)"
        @toggleInspector="[handleToggleInspector($event), setInspectorButtonPosition($event)]"
      />

      <component
        v-for="node in nodes"
        :is="node.type"
        :key="node._modelerId"
        ref="nodeComponent"
        :graph="graph"
        :paper="paper"
        :node="node"
        :id="node.id"
        :highlighted="highlightedNodes.includes(node)"
        :has-error="invalidNodes.includes(node)"
        :border-outline="borderOutline(node.id)"
        :collaboration="collaboration"
        :process-node="processNode"
        :processes="processes"
        :plane-elements="planeElements"
        :moddle="moddle"
        :nodeRegistry="nodeRegistry"
        :root-elements="definitions?.get('rootElements')"
        :isRendering="isRendering"
        :paperManager="paperManager"
        :auto-validate="autoValidate"
        :node-id-generator="nodeIdGenerator"
        :is-active="node === activeNode"
        :is-completed="requestCompletedNodes.includes(node.definition.id)"
        :is-in-progress="requestInProgressNodes.includes(node.definition.id)"
        :is-idle="requestIdleNodes.includes(node.definition.id)"
        :custom-dropdown-data="customDropdownData"
        @add-node="addNode"
        @remove-node="removeNode"
        @previewNode="[handlePreview($event), setInspectorButtonPosition($event)]"
        @set-cursor="cursor = $event"
        @set-pool-target="poolTarget = $event"
        @unset-pools="unsetPools"
        @clearSelection="clearSelection"
        @set-pools="setPools"
        @save-state="pushToUndoStack"
        @set-shape-stacking="setShapeStacking"
        @setTooltip="tooltipTarget = $event"
        @replace-node="replaceNode"
        @replace-generic-flow="replaceGenericFlow"
        @copy-element="copyElement"
        @copy-selection="copyElement"
        @paste-element="pasteElements"
        @clone-element="cloneElement"
        @clone-selection="cloneSelection"
        @default-flow="toggleDefaultFlow"
        @shape-resize="shapeResize"
        @definition-changed="onNodeDefinitionChanged"
      />

      <RailBottom
        v-show="!loadingAI"
        :nodeTypes="nodeTypes"
        :paper-manager="paperManager"
        :graph="graph"
        :is-rendering="isRendering"
        :pan-mode="panMode"
        @load-xml="loadXML"
        @clearSelection="clearSelection"
        @set-cursor="cursor = $event"
        @onCreateElement="onCreateElementHandler"
        @set-pan-mode="panMode = $event"
      />

      <selection
        v-if="paper"
        ref="selector"
        :graph="graph"
        :paperManager="paperManager"
        :useModelGeometry="false"
        @clone-selection="cloneSelection"
        @remove-nodes="removeNodes"
        :processNode="processNode"
        @save-state="pushToUndoStack"
        :isMultiplayer="isMultiplayer"
      />
      <RemoteCursor
        v-for="player in filteredPlayers"
        :key="player.id"
        :data="prepareCursorData(player)"
      />
    </b-row>
  </span>
</template>

<script>
import { shapes } from 'jointjs';
import Vue from 'vue';
import NodeDocumentation from '../documenting/NodeDocumentation.vue';
import _ from 'lodash';
import { dia } from 'jointjs';
import boundaryEventConfig from '../nodes/boundaryEvent';
import BpmnModdle from 'bpmn-moddle';
import ExplorerRail from '../rails/explorer-rail/explorer';
import WelcomeMessage from '../welcome/WelcomeMessage.vue';
import CreateAssetsCard from '../aiMessages/CreateAssetsCard.vue';
import GeneratingAssetsCard from '../aiMessages/GeneratingAssetsCard.vue';
import AssetsCreatedCard from '../aiMessages/AssetsCreatedCard.vue';
import CreateAssetsFailCard from '../aiMessages/CreateAssetsFailCard.vue';
import { isJSON } from 'lodash-contrib';
import pull from 'lodash/pull';
import remove from 'lodash/remove';
import store from '@/store';
import nodeTypesStore from '@/nodeTypesStore';
import InspectorButton from '@/components/inspectors/inspectorButton/InspectorButton.vue';
import InspectorPanel from '@/components/inspectors/InspectorPanel';
import PreviewPanel from '@/components/inspectors/PreviewPanel';
import undoRedoStore from '@/undoRedoStore';
import { Linter } from 'bpmnlint';
import linterConfig from '../../../.bpmnlintrc';
import { getNodeIdGenerator } from '../../NodeIdGenerator';
import Process from '../inspectors/process';
import runningInCypressTest from '@/runningInCypressTest';
import getValidationProperties from '@/targetValidationUtils';
import { id as laneId } from '@/components/nodes/poolLane/config';
import { id as processId } from '@/components/inspectors/process';
import { id as sequenceFlowId } from '../nodes/sequenceFlow';
import { id as associationId } from '../nodes/association/associationConfig';
import { id as messageFlowId } from '../nodes/messageFlow/config';
import { id as dataOutputAssociationFlowId } from '../nodes/dataOutputAssociation/config';
import { id as dataInputAssociationFlowId } from '../nodes/dataInputAssociation/config';
import { id as genericFlowId } from '@/components/nodes/genericFlow/config';
import { id as boundaryErrorEventId } from '@/components/nodes/boundaryErrorEvent';
import { id as boundaryConditionalEventId } from '@/components/nodes/boundaryConditionalEvent';
import { id as boundaryEscalationEventId } from '@/components/nodes/boundaryEscalationEvent';
import { id as boundaryMessageEventId } from '@/components/nodes/boundaryMessageEvent';
import { id as boundarySignalEventId } from '@/components/nodes/boundarySignalEvent';
import { id as boundaryTimerEventId } from '@/components/nodes/boundaryTimerEvent';

import PaperManager from '../paperManager';
import registerInspectorExtension from '@/components/InspectorExtensionManager';

import ensureShapeIsNotCovered from '@/components/shapeStackUtils';
import ToolBar from '@/components/toolbar/ToolBar';
import Node from '@/components/nodes/node';
import { addNodeToProcess } from '@/components/nodeManager';
import hotkeys from '@/components/hotkeys/main';
import TimerEventNode from '@/components/nodes/timerEventNode';
import focusNameInputAndHighlightLabel from '@/components/modeler/focusNameInputAndHighlightLabel';
import XMLManager from '@/components/modeler/XMLManager';
import {
  removeBoundaryEvents,
  removeNodeAssociations,
  removeNodeFlows,
  removeNodeMessageFlows,
  removeOutgoingAndIncomingRefsToFlow,
  removeSourceDefault,
} from '@/components/crown/utils';
import { getInvalidNodes } from '@/components/modeler/modelerUtils';
import { NodeMigrator } from '@/components/modeler/NodeMigrator';
import addLoopCharacteristics from '@/setup/addLoopCharacteristics';
import cloneSelection from '../../mixins/cloneSelection';
import linkEditing from '../../mixins/linkEditing';
import transparentDragging from '@/mixins/transparentDragging';
import RailBottom from '@/components/railBottom/RailBottom.vue';

import ProcessmakerModelerGenericFlow from '@/components/nodes/genericFlow/genericFlow';

import Selection from './Selection';
import RemoteCursor from '@/components/multiplayer/remoteCursor/RemoteCursor.vue';
import Multiplayer from '@/multiplayer/multiplayer';
import { getBoundaryEventData } from '../nodes/boundaryEvent/boundaryEventUtils';
import validPreviewElements from '@/components/crown/crownButtons/validPreviewElements';

export default {
  components: {
    NodeDocumentation,
    PreviewPanel,
    ToolBar,
    ExplorerRail,
    InspectorButton,
    InspectorPanel,
    ProcessmakerModelerGenericFlow,
    Selection,
    RailBottom,
    WelcomeMessage,
    CreateAssetsCard,
    GeneratingAssetsCard,
    AssetsCreatedCard,
    CreateAssetsFailCard,
    RemoteCursor,
  },
  props: {
    owner: Object,
    decorations: {
      type: Object,
      default() {
        return {};
      },
    },
    readOnly: {
      type: Boolean,
      default() {
        return false;
      },
    },
    validationBar: Array,
    requestIdleNodes: {
      type: Array,
      default: () => [],
    },
    requestCompletedNodes: {
      type: Array,
      default: () => [],
    },
    requestInProgressNodes: {
      type: Array,
      default: () => [],
    },
    /**
     * When true the toolbar is displayed inside the modeler
     */
    showToolbar: {
      type: Boolean,
      default: true,
    },
    forDocumenting: {
      type: Boolean,
      default() {
        return false;
      },
    },
    forPrinting: {
      type: Boolean,
      default() {
        return false;
      },
    },
    /**
     * When true the modeler is in preview mode (Overview and Slideshow)
     */
    preview: {
      type: Boolean,
      default() {
        return false;
      },
    },
  },
  mixins: [hotkeys, cloneSelection, linkEditing, transparentDragging],
  data() {
    return {
      extraActions: [],
      pasteInProgress: false,
      cloneInProgress: false,
      internalClipboard: [],
      tooltipTarget: null,

      /* Custom parsers for handling certain bpmn node types */
      parsers: {},

      // What bpmn moddle extensions should we register
      extensions: [],
      // Our node types, keyed by the id
      nodeRegistry: {},

      // Our jointjs data graph model
      graph: null,
      paper: null,
      paperManager: null,

      definitions: null,
      nodeIdGenerator: null,
      planeElements: null,
      canvasDragPosition: null,
      processNode: null,
      collaboration: null,
      moddle: null,
      allowDrop: true,
      poolTarget: null,
      processes: [],
      cursor: null,
      parentHeight: null,
      parentWidth: null,
      linter: null,
      validationErrors: {},
      miniMapOpen: false,
      panelsCompressed: false,
      isOpenInspector: false,
      isOpenPreview: false,
      panMode: false,
      panning: false,
      isRendering: false,
      isLoaded: false,
      allWarnings: [],
      nodeTypes: [],
      pmBlockNodes: [],
      breadcrumbData: [],
      activeNode: null,
      xmlManager: null,
      previouslyStackedShape: null,
      keyMod: this.isAppleOS() ? 'Command' : 'Control',
      canvasScale: 1,
      initialScale: 1,
      minimumScale: 0.2,
      scaleStep: 0.1,
      isDragging: false,
      isSelecting: false,
      isIntoTheSelection: false,
      dragStart: null,
      players: [],
      showInspectorButton: true,
      inspectorButtonRight: 65,
      inspectorButtonTopHeight: 95,
      previewConfigs: [],
      isResizingPreview: false,
      currentCursorPosition: 0,
      previewPanelWidth: 600,
      isAiGenerated: window.ProcessMaker?.modeler?.isAiGenerated,
      assetsCreated: false,
      assetFail: false,
      currentNonce: null,
      promptSessionId: '',
      loadingAI: false,
      cancelledJobs: [],
      flowTypes: [
        'processmaker-modeler-sequence-flow',
        'processmaker-modeler-message-flow',
        'processmaker-modeler-data-input-association',
        'processmaker-modeler-data-output-association',
        'processmaker-modeler-association',
      ],
      boundaryEventTypes: [
        'processmaker-modeler-boundary-timer-event',
        'processmaker-modeler-boundary-error-event',
        'processmaker-modeler-boundary-signal-event',
        'processmaker-modeler-boundary-conditional-event',
        'processmaker-modeler-boundary-message-event',
      ],
      validPreviewElements,
      centered: false,
      customDropdownData: [],
      currentStageModel: null,
    };
  },
  watch: {
    isReadOnly() {
      this.panMode = this.isReadOnly;
    },
    panning() {
      if (!this.panning) {
        this.canvasDragPosition = null;
      }
    },
    isRendering() {
      const loadingMessage = 'Loading process, please be patient.';
      if (this.isRendering) {
        window.ProcessMaker.alert(loadingMessage, 'warning');
        document.body.style.cursor = 'wait !important';
        this.cursor = 'wait';
        return;
      }

      window.ProcessMaker.navbar.alerts = window.ProcessMaker.navbar.alerts.filter(alert => {
        return alert.alertText !== loadingMessage;
      });
      document.body.style.cursor = 'auto';
      this.cursor = null;

      this.isLoaded = true;
    },
    currentXML() {
      this.validateIfAutoValidateIsOn();
    },
    definitions() {
      this.validateIfAutoValidateIsOn();
    },
    autoValidate() {
      this.validateIfAutoValidateIsOn();
    },
    canvasScale(canvasScale) {
      this.paperManager.scale = canvasScale;
    },
    highlightedNodes() {
      this.applyPreviewVisibilityForSelection();
      if (window.ProcessMaker?.EventBus) {
        window.ProcessMaker.EventBus.$emit('modeler:highlightedNodes', this.highlightedNodes);
      }
    },
    creatingNewNode() {
      if (this.creatingNewNode) {
        this.clearSelection();
      }
    },
  },
  computed: {
    isReadOnly() {
      return store.getters.isReadOnly;
    },
    isForDocumenting() {
      return store.getters.isForDocumenting;
    },
    creatingNewNode() {
      return nodeTypesStore.getters.getSelectedNode;
    },
    filteredPlayers() {
      const allPlayers = _.uniqBy(this.players, 'name');
      return allPlayers.filter(player => {
        return player.name.toLowerCase() !== window.ProcessMaker.user?.fullName.toLowerCase();
      });
    },
    showWelcomeMessage() {
      return !this.selectedNode &&
        !this.nodes.length &&
        !this.isReadOnly &&
        this.isLoaded &&
        !undoRedoStore.getters.isRunning &&
        this.isPackageAiInstalled;
    },
    noElementsSelected() {
      return this.highlightedNodes.filter(node => !node.isType('processmaker-modeler-process')).length === 0;
    },
    tooltipTitle() {
      if (this.tooltipTarget) {
        return this.tooltipTarget.$el.data('title');
      }
      return undefined;
    },
    autoValidate: () => store.getters.autoValidate,
    nodes: () => store.getters.nodes,
    currentXML() {
      return undoRedoStore.getters.currentState;
    },
    copiedElements: () => store.getters.copiedElements,
    /* connectors expect a highlightedNode property */
    highlightedNode: () => store.getters.highlightedNodes[0],
    highlightedNodes: () => store.getters.highlightedNodes,
    invalidNodes() {
      return getInvalidNodes(this.validationErrors, this.nodes);
    },
    showComponent: () => store.getters.showComponent,
    isMultiplayer: () => store.getters.isMultiplayer,
    isPackageAiInstalled() {
      return window.ProcessMaker?.modeler?.isPackageAiInstalled;
    },
  },
  methods: {
    getSvg() {
      const svg = document.querySelector('.mini-paper svg');
      const css = 'text { font-family: sans-serif; }';
      const style = document.createElement('style');
      style.appendChild(document.createTextNode(css));
      svg.appendChild(style);
      const svgString = (new XMLSerializer()).serializeToString(svg);
      return svgString;
    },
    mountedInit() {
      store.commit('setReadOnly', this.readOnly);
      store.commit('setForDocumenting', this.forDocumenting);
      store.commit('setForPrinting', this.forPrinting);
      this.graph = new dia.Graph();
      store.commit('setGraph', this.graph);
      this.graph.set('interactiveFunc', cellView => {
        const isPoolEdge = cellView.model.get('type') === 'standard.EmbeddedImage';
        return {
          elementMove: isPoolEdge,
          labelMove: false,
        };
      });

      this.paperManager = PaperManager.factory(this.$refs.paper, this.graph.get('interactiveFunc'), this.graph);
      this.paper = this.paperManager.paper;
    },
    setCardPosition(docNode) {
      return { x: docNode.position.x, y: docNode.position.y };
    },
    addEventHandlers() {
      window.ProcessMaker.EventBus.$on('show-documentation', (event) => {
        if (this.$refs['nodeDocumentation'] && this.$refs['nodeDocumentation'].isVisible === false) {
          this.$refs['nodeDocumentation'].text = event.text;
          this.$refs['nodeDocumentation'].number = event.number;
          this.$refs['nodeDocumentation'].position = this.setCardPosition(event);
          this.$refs['nodeDocumentation'].elementType = event.node.definition.$type.replace('bpmn:', '');
          this.$refs['nodeDocumentation'].elementTitle = event.node.definition.name;
          this.$refs['nodeDocumentation'].elementImplementation = event.node.definition.implementation;
          this.$refs['nodeDocumentation'].elementCalledElement = event.node.definition.calledElement;
          this.$refs['nodeDocumentation'].elementConfig = event.node.definition.config;
          this.$refs['nodeDocumentation'].isVisible = true;
          this.$refs['nodeDocumentation'].event = event;

          event.view.model.attr({
            doclabel: {
              text: event.number,
              'ref-x': (95 - String(event.number).length * 2),
              display: 'block',
            },
          });

          // if it is a link
          if (event?.view?.path?.segments) {
            const firstSegment = event.view.path.segments[0];
            const diffX = Math.floor(firstSegment.nextSegment.end.x) - Math.floor(firstSegment.end.x);
            const diffY = Math.floor(firstSegment.nextSegment.end.y) - Math.floor(firstSegment.end.y);
            const deltaX = diffX == 0 ? -17 : -Math.sign(diffX) * 5;
            const deltaY = diffY == 0 ? -17 : -Math.sign(diffY) * 5;

            const circle = new shapes.basic.Circle({
              id: 'sequenceDocCircle',
              position: { x: event.view.sourcePoint.x + deltaX, y: event.view.sourcePoint.y + deltaY },
              size: { width: 40, height: 40 },

              attrs: {
                circle: {
                  fill:  '#1572C2',
                  strokeWidth: 0,
                },
                text: { text: event.number , fill: 'white', fontSize: 20, fontWeight: 'bold'},
              },
            });

            circle.addTo(this.graph);
          }

          if (event.view?.path?.segments === undefined) {
            event.view.model.attr({
              doccircle: {
                r: 20,
                fill: '#1572C2',
                strokeWidth: 0,
              },
              doclabel: {
                display: 'block',
              },
            });
          }

        }
      });

      window.ProcessMaker.EventBus.$on('hide-documentation', (event) => {
        if (this.$refs['nodeDocumentation']) {
          this.$refs['nodeDocumentation'].text = '';
          this.$refs['nodeDocumentation'].number = null;
          this.$refs['nodeDocumentation'].isVisible = false;
        }

        if (event.view?.model?.attributes?.attrs?.doccircle) {
          event.view.model.attr({
            doccircle: {
              r: 10,
              stroke: '#2B9DFF',
              strokeWidth: '3',
              fill: '#8DC8FF',
            },
            doclabel: {
              display:'none',
            },
          });
        }


        const cell = this.graph.getCell('sequenceDocCircle');
        if (cell) {
          cell.remove();
        }
      });



      this.paperManager.addEventHandler('cell:pointerdblclick', focusNameInputAndHighlightLabel);

      this.handleResize();
      window.addEventListener('resize', this.handleResize);

      store.commit('setPaper', this.paperManager.paper);

      this.paperManager.addEventHandler('element:pointerclick', this.blurFocusedScreenBuilderElement, this);

      this.paperManager.addEventHandler('blank:pointerdown', (event) => {
        if (this.panMode) {
          this.startPanning();
          return;
        }
        this.pointerDownHandler(event);
      }, this);

      this.paperManager.addEventHandler('blank:pointerup', (event) => {
        if (this.panMode) {
          this.stopPanning();
        }
        this.activeNode = null;
        this.pointerUpHandler(event);
      }, this);
      this.paperManager.addEventHandler('cell:pointerup', (cellView, event) => {
        // If panMode is true, stop panning. But do not return if we are in read-only mode.
        if (this.panMode) {
          this.stopPanning();
          if (!this.isReadOnly) {
            return;
          }
        }
        this.activeNode = null;
        this.pointerUpHandler(event, cellView);
      }, this);

      this.$refs['paper-container'].addEventListener('mouseenter', () => {
        store.commit('setClientLeftPaper', false);
      });

      this.$el.addEventListener('mousemove', event => {
        this.pointerMoveHandler(event);
      });

      this.$refs['paper-container'].addEventListener('mouseleave', () => {
        this.paperManager.removeEventHandler('blank:pointermove');
        store.commit('setClientLeftPaper', true);
      });

      this.paperManager.addEventHandler('cell:pointerclick', (cellView, evt, x, y) => {
        const clickHandler = cellView.model.get('onClick');
        if (clickHandler) {
          clickHandler(cellView, evt, x, y);
        }
      });

      this.paperManager.addEventHandler('cell:pointerclick', ({ model: shape }, event) => {
        if (!this.isBpmnNode(shape)) {
          return;
        }

        // Ignore click event if the user is Grabbing the paper, only when isReadOnly is false.
        if (!this.isReadOnly && this.panMode) {
          return;
        }

        shape.component.$emit('click', event);

        // Use nextTick to ensure that the node is highlighted before emitting the click event.
        this.$nextTick(() => {
          this.$emit('click', {
            event,
            node: this.highlightedNode.definition,
          });
        });
      });

      this.paperManager.addEventHandler('cell:pointerdown', ({ model: shape }, event) => {
        if (!this.isBpmnNode(shape)) {
          return;
        }
        // If panMode is true, start panning. But do not return if we are in read-only mode.
        if (this.panMode) {
          this.startPanning();
          if (!this.isReadOnly) {
            return;
          }
        }
        this.setShapeStacking(shape);
        this.activeNode = shape.component.node;
        this.pointerDowInShape(event, shape);
      });

      this.$root.$on('replace-ai-node', (data) => {
        this.replaceAiNode(data);
      });

      window.ProcessMaker.EventBus.$on('save-changes', (redirectUrl, nodeId, generatingAssets) => {
        if (redirectUrl) {
          this.redirect(redirectUrl);
        }
        if (!nodeId && generatingAssets) {
          this.generateAssets();
        }
      });

      this.paperManager.paper.on('link:pointerclick', (linkView) => {
        this.currentStageModel = linkView.model;
      });
    },
    getCurrentStageModelComponent() {
      return this.currentStageModel.component;
    },
    registerCustomNodes()
    {
      /* Register custom nodes */
      window.ProcessMaker.EventBus.$emit('modeler-start', {
        $t: this.$t,
        modeler: this,
        registerMenuAction: this.registerMenuAction,
        loadXML: async(xml) => {
          await this.loadXML(xml);
          await undoRedoStore.dispatch('pushState', xml);

          try {
            const multiplayer = new Multiplayer(this);
            multiplayer.init();
            this.multiplayer = multiplayer;
          } catch (error) {
            console.warn('Could not initialize multiplayer', error);
          }
        },
        addWarnings: warnings => this.$emit('warnings', warnings),
        addBreadcrumbs: breadcrumbs => this.breadcrumbData.push(breadcrumbs),
      });
    },
    initAI() {
      // AI Setup
      if (this.isPackageAiInstalled) {
        this.currentNonce = localStorage.currentNonce;
        if (!localStorage.getItem('promptSessions') || localStorage.getItem('promptSessions') === 'null') {
          localStorage.setItem('promptSessions', JSON.stringify([]));
        }
        if (!localStorage.getItem('cancelledJobs') || localStorage.getItem('cancelledJobs') === 'null') {
          this.cancelledJobs = [];
        } else {
          this.cancelledJobs = JSON.parse(localStorage.getItem('cancelledJobs'));
        }
        this.promptSessionId = this.getPromptSessionForUser();
        this.fetchHistory();
        this.$nextTick(() => {
          this.subscribeToProgress();
          this.subscribeToGenerationCompleted();
          this.subscribeToErrors();
        });
      }
    },
    onNodeDefinitionChanged() {
      // re-render the preview just if the preview pane is open
      if (this.isOpenPreview) {
        this.handlePreview();
      }
      // Ensure validations are up to date when a node definition changes.
      this.safeRevalidate();
    },
    onStartPreviewResize(event) {
      this.isResizingPreview = true;
      this.currentCursorPosition = event.x;
    },
    onMouseUp(event) {
      if (window.ProcessMaker.mouseDownDrag) {
        window.ProcessMaker.EventBus.$emit('custom-pointerclick', event);
        window.ProcessMaker.mouseDownDrag = false;
      }
      this.isResizingPreview = false;
    },
    onMouseMove(event) {
      if (this.isResizingPreview) {
        const dx = this.currentCursorPosition - event.x;
        this.previewPanelWidth = parseInt(this.previewPanelWidth) + dx;
        this.currentCursorPosition = event.x;
      }
    },
    registerPreview(config) {
      this.previewConfigs.push(config);
    },
    registerCustomDropdownData(node, config) {
      node.items.push(config);

      this.customDropdownData.push(this.prepareCustomDropdownData(config));
    },
    prepareCustomDropdownData(config) {
      return {
        label: config.label,
        nodeType: config.id,
        dataTest: config.dataTest,
      };
    },
    handleToolbarAction(action) {
      if (action.handler instanceof Function) {
        action.handler(this);
      }
    },
    handleToggleInspector(value) {
      this.showInspectorButton = !(value ?? true);
      this.isOpenInspector = value;
    },
    handlePreview() {
      if (this.highlightedNodes.length != 1) {
        this.isOpenPreview = false;
      }
      this.$refs['preview-panel'].previewNode(true);
      this.handleTogglePreview(true) ;
    },
    handleTogglePreview(value) {
      this.isOpenPreview = value;
    },
    setInspectorButtonPosition() {
      if (this.isOpenInspector) {
        return;
      }

      if (window.ProcessMaker.AbTesting) {
        this.inspectorButtonTopHeight = 30;
      }

      if (this.isOpenPreview && !this.isOpenInspector) {
        // the scaling is optimized for 1920px width. Other resolutions will be adjusted accordingly
        const delta = screen.width / 1920;
        this.inspectorButtonRight = 65 * delta + this.previewPanelWidth;
      }

      if (!this.isOpenPreview && !this.isOpenInspector) {
        this.inspectorButtonRight = 65;
      }
    },
    isAppleOS() {
      return typeof navigator !== 'undefined' && /Mac|iPad|iPhone/.test(navigator.platform);
    },
    async shapeResize(clearIfEmpty=true) {
      await this.$nextTick();
      await this.paperManager.awaitScheduledUpdates();
      this.$refs.selector.updateSelectionBox(true, clearIfEmpty);
    },
    toggleDefaultFlow(flow) {
      const source = flow.definition.sourceRef;
      if (source.default && source.default.id === flow.id) {
        flow = null;
      }
      window.ProcessMaker.EventBus.$emit('multiplayer-updateNodes', [
        {
          id: source.id,
          properties: {
            default: flow?.id || null,
          },
        },
      ]);
      source.set('default', flow);
    },
    cloneElement(node, copyCount) {
      const clonedNode = node.clone(this.nodeRegistry, this.moddle, this.$t);
      const yOffset = (node.diagram.bounds.height + 30) * copyCount;

      clonedNode.diagram.bounds.y += yOffset;
      this.addNode(clonedNode);

      this.$emit('cloneElement', clonedNode);
    },
    copyElement() {
      // Checking if User selected a single flow and tries to copy it, to deny it.
      const flows = [
        sequenceFlowId,
        dataOutputAssociationFlowId,
        dataInputAssociationFlowId,
        genericFlowId,
        processId,
      ];
      const boundaryEvents = [
        boundaryErrorEventId,
        boundaryConditionalEventId,
        boundaryEscalationEventId,
        boundaryMessageEventId,
        boundarySignalEventId,
        boundaryTimerEventId,
      ];
      if (this.highlightedNodes.length === 1 && (flows.includes(this.highlightedNodes[0].type) || boundaryEvents.includes(this.highlightedNodes[0].type))) return;
      store.commit('setCopiedElements', this.cloneNodesSelection());
      this.$bvToast.toast(this.$t('Object(s) have been copied'), { noCloseButton:true, variant: 'success', solid: true, toaster: 'b-toaster-top-center' });
    },
    publishTemplate() {
      this.$emit('publishTemplate');
    },
    publishPmBlock() {
      this.$emit('publishPmBlock');
    },
    async pasteElements() {
      if (this.copiedElements.length > 0 && !this.pasteInProgress) {
        this.pasteInProgress = true;
        try {
          await this.addClonedNodes(this.copiedElements);
          await this.$nextTick();
          await this.paperManager.awaitScheduledUpdates();
          await this.$refs.selector.selectElements(this.findViewElementsFromNodes(this.copiedElements), true);
          await this.$nextTick();
          await store.commit('setCopiedElements', this.cloneNodesSelection());
          this.scrollToSelection();
        } finally {
          this.pasteInProgress = false;
          await this.pushToUndoStack();
        }
      }
    },
    async cloneSelection() {
      const clonedNodes = this.cloneNodesSelection();
      if (clonedNodes && clonedNodes.length === 0) {
        return;
      }
      try {
        this.cloneInProgress = true;
        this.$refs.selector.clearSelection();
        await this.addClonedNodes(clonedNodes);
        await this.$nextTick();
        await this.paperManager.awaitScheduledUpdates();
        await this.$refs.selector.selectElements(this.findViewElementsFromNodes(clonedNodes));
        this.scrollToSelection();
      } finally {
        this.cloneInProgress = false;
        await this.pushToUndoStack();
      }
    },
    scrollToSelection() {
      const containerRect = this.$refs['paper-container'].getBoundingClientRect();
      const selector = this.$refs.selector;
      const selectorRect = selector.$el.getBoundingClientRect();
      // Scroll to the cloned elements only when they are not visible on the screen.
      if (selectorRect.right > containerRect.right || selectorRect.bottom > containerRect.bottom || selectorRect.left < containerRect.left || selectorRect.top < containerRect.top) {
        const currentPosition = this.paper.translate();
        const newTy = currentPosition.ty - (selectorRect.top - containerRect.top - selectorRect.height);
        this.paper.translate(currentPosition.tx, newTy);
        selector.updateSelectionBox(true);
      }
    },
    findViewElementsFromNodes(nodes) {
      return nodes.map(node => {
        const component = this.$refs.nodeComponent.find(cmp => cmp.node === node);
        const shape = component.shape;
        return this.paper.findViewByModel(shape);
      });
    },
    getElementByNodeId(id) {
      const cells = this.paper.model.getCells();
      return cells.find((cell) => cell.component && cell.component.id === id);
    },
    async close() {
      this.$emit('close');
    },
    async saveBpmn(redirectTo = null, id = null, generatingAssets = false) {
      const svg = document.querySelector('.mini-paper svg');
      const css = 'text { font-family: sans-serif; }';
      const style = document.createElement('style');
      style.appendChild(document.createTextNode(css));

      svg.appendChild(style);
      const xml = await this.getXmlFromDiagram();
      const svgString = (new XMLSerializer()).serializeToString(svg);
      this.$emit('saveBpmn', { xml, svg: svgString, redirectUrl: redirectTo, nodeId: id, generatingAssets });
    },
    borderOutline(nodeId) {
      return this.decorations.borderOutline && this.decorations.borderOutline[nodeId];
    },
    addWarning(warning) {
      this.allWarnings.push(warning);
      this.$emit('warnings', this.allWarnings);
    },
    validatePlaneElements() {
      this.planeElements
        .filter(diagram => !diagram.bpmnElement)
        .map(diagram => ({
          title: this.$t('Non-existent Element'),
          text: this.$t('bpmdi:BPMNShape ') + diagram.id + this.$t(' references a non-existent element and was not parsed'),
        }))
        .forEach(warning => this.addWarning(warning));
    },
    cleanPlaneElements() {
      remove(this.planeElements, diagram => !diagram.bpmnElement);
    },
    getTooltipTarget() {
      return this.tooltipTarget.$el[0];
    },
    validateIfAutoValidateIsOn() {
      if (this.autoValidate) {
        this.safeRevalidate();
      }
    },
    translateConfig(inspectorConfig) {
      if (inspectorConfig.config) {
        const config = inspectorConfig.config;

        config.label = this.$t(config.label);
        config.helper = this.$t(config.helper);
      }

      if (inspectorConfig.items) {
        inspectorConfig.items.forEach(this.translateConfig);
      }
    },
    async pushToUndoStack() {
      if (this.pasteInProgress || this.cloneInProgress) {
        return;
      }
      try {
        const xml = await this.getXmlFromDiagram();
        await undoRedoStore.dispatch('pushState', xml);
        window.ProcessMaker.EventBus.$emit('modeler-change');
      } catch (invalidXml) {
        // eslint-disable-next-line no-console
        console.warn(invalidXml.message);
      }
    },
    getXmlFromDiagram() {
      return new Promise((resolve, reject) => {
        this.toXML((error, xml) => {
          if (error) {
            reject(error);
          } else {
            resolve(xml);
          }
        });
      });
    },
    async validateBpmnDiagram() {
      if (!store.getters.globalProcesses || store.getters.globalProcesses.length === 0) {
        await store.dispatch('fetchGlobalProcesses');
      }

      this.validationErrors = await this.linter.lint(this.definitions);
      
      // Filter out false positive validation errors for properly configured PM Blocks
      this.filterFalsePositivePMBlockErrors();
      
      this.$emit('validate', this.validationErrors);
    },
    filterFalsePositivePMBlockErrors() {
      // Filter out false positive validation errors for properly configured PM Blocks
      if (this.validationErrors['processmaker/call-activity-child-process']) {
        const originalErrors = this.validationErrors['processmaker/call-activity-child-process'];
        const filteredErrors = originalErrors.filter(error => {
          // Find the corresponding node
          const node = this.nodes.find(n => n.definition.id === error.id);
          if (!node || !node.isBpmnType('bpmn:CallActivity')) {
            return true; // Keep the error if we can't find the node
          }
          
          // Check if the PM Block is properly configured
          const hasCalledElement = node.definition.calledElement && node.definition.calledElement !== '';
          const hasConfig = node.definition.config && node.definition.config !== '{}';
          
          if (!hasCalledElement || !hasConfig) {
            return true; // Keep the error if the PM Block is genuinely unconfigured
          }
          
          // Parse the config and check for required fields
          try {
            const config = JSON.parse(node.definition.config);
            const hasProcessId = config.processId && config.processId !== null;
            const hasStartEvent = config.startEvent && config.startEvent !== null;
            
            // If PM Block is properly configured, remove the error; otherwise keep it
            return !(hasProcessId && hasStartEvent);
          } catch (error) {
            if (error instanceof SyntaxError) {
              return true; // Keep the error if config is invalid JSON or parsing fails
            }
            throw error;
          }
        });
        
        this.validationErrors['processmaker/call-activity-child-process'] = filteredErrors;
      }
    },
    setPools(poolDefinition) {
      if (!this.collaboration) {
        this.collaboration = this.moddle.create('bpmn:Collaboration');
        this.definitions.get('rootElements').push(this.collaboration);
        this.collaboration.set('id', 'collaboration_0');
        this.plane.set('bpmnElement', this.collaboration);
      }

      if (poolDefinition.get('processRef')) {
        if (!this.collaboration.get('participants').includes(poolDefinition)) {
          this.collaboration.get('participants').push(poolDefinition);
        }

        return;
      }

      let process;
      if (this.collaboration.get('participants').length === 0) {
        process = this.processNode.definition;
      } else {
        process = this.moddle.create('bpmn:Process');
        this.processes.push(process);
        process.set('id', this.nodeIdGenerator.generateProcessId());
        this.definitions.get('rootElements').push(process);
      }

      poolDefinition.set('processRef', process);
      this.collaboration.get('participants').push(poolDefinition);
    },
    unsetPools() {
      pull(this.definitions.get('rootElements'), this.collaboration);
      this.plane.set('bpmnElement', this.processNode.definition);
      this.collaboration = null;
    },
    highlightNode(node, event) {
      if (!node || !this.highlightedNode) {
        return;
      }

      if (event && event.shiftKey) {
        store.commit('addToHighlightedNodes', [node]);
        return;
      }

      let isSameHighlightedNode = _.isEqual(node.id, this.highlightedNode.id);

      if (!isSameHighlightedNode) {
        store.commit('highlightNode', node);
      }

      return;
    },
    blurFocusedScreenBuilderElement() {
      const elementsToBlur = ['INPUT', 'SELECT'];
      if (elementsToBlur.includes(document.activeElement && document.activeElement.tagName)) {
        document.activeElement.blur();
      }
    },
    registerStatusBar(component) {
      this.owner.validationBar.push(component);
    },
    /**
     * Register a mixin into a node component.
     * Used during "modeler-before-init"
     *
     */
    registerComponentMixin(component, mixin) {
      if (!component.mixins) {
        component.mixins = [];
      }
      component.mixins.push(mixin);
    },
    /**
     * Register a BPMN Moddle extension in order to support extensions to the bpmn xml format.
     * This is used to support new attributes and elements that would be needed for specific
     * bpmn execution environments.
     */
    registerBpmnExtension(namespace, extension) {
      this.extensions[namespace] = extension;
    },
    registerNode(nodeType, customParser) {
      const defaultParser = () => nodeType.id;
      const implementationParser = definition => {
        if (definition.get('implementation') === nodeType.implementation) {
          return nodeType.id;
        }
        return undefined;
      };

      this.translateConfig(nodeType.inspectorConfig[0]);
      addLoopCharacteristics(nodeType);
      this.nodeRegistry[nodeType.id] = nodeType;
      Vue.component(nodeType.id, nodeType.component);
      this.nodeTypes.push(nodeType);

      const types = Array.isArray(nodeType.bpmnType)
        ? nodeType.bpmnType
        : [nodeType.bpmnType];

      types.forEach(bpmnType => {
        if (!this.parsers[bpmnType]) {
          this.parsers[bpmnType] = { custom: [], implementation: [], default: []};
        }

        if (customParser) {
          this.parsers[bpmnType].custom.push(customParser);
          return;
        }

        if (nodeType.implementation) {
          this.parsers[bpmnType].implementation.push(implementationParser);
          return;
        }

        this.parsers[bpmnType].default.push(defaultParser);
      });
      nodeTypesStore.commit('setNodeTypes', this.nodeTypes);
    },
    registerPmBlock(pmBlockNode, customParser) {
      const defaultParser = () => pmBlockNode.id;

      this.translateConfig(pmBlockNode.inspectorConfig[0]);
      addLoopCharacteristics(pmBlockNode);
      this.nodeRegistry[pmBlockNode.id] = pmBlockNode;

      Vue.component(pmBlockNode.id, pmBlockNode.component);
      this.pmBlockNodes.push(pmBlockNode);

      const types = Array.isArray(pmBlockNode.bpmnType)
        ? pmBlockNode.bpmnType
        : [pmBlockNode.bpmnType];

      types.forEach(bpmnType => {
        if (!this.parsers[bpmnType]) {
          this.parsers[bpmnType] = { custom: [], implementation: [], default: []};
        }

        if (customParser) {
          this.parsers[bpmnType].custom.push(customParser);
          return;
        }
        this.parsers[bpmnType].default.push(defaultParser);
      });
      nodeTypesStore.commit('setPmBlockNodeTypes', this.pmBlockNodes);
    },
    registerMenuAction(action) {
      if (!action.value || typeof action.value !== 'string') {
        throw new Error('Menu action must have a action.value');
      }
      if (!action.content || typeof action.content !== 'string') {
        throw new Error('Menu action must have a action.content');
      }
      this.extraActions.push(action);
    },
    addMessageFlows() {
      if (this.collaboration) {
        this.collaboration
          .get('messageFlows')
          .filter(this.hasSourceAndTarget)
          .forEach(this.setNode);
      }
    },
    loadAssociations(flowElements, artifacts) {
      artifacts
        .filter(definition => definition.$type === 'bpmn:Association' && this.hasSourceAndTarget(definition))
        .forEach(definition => this.setNode(definition, flowElements, artifacts));
    },
    loadDataAssociations(flowElements) {
      const tasksThatHaveDataOutputAssociations = flowElements.filter(task => task.get('dataOutputAssociations') &&
          task.get('dataOutputAssociations').length > 0);

      tasksThatHaveDataOutputAssociations.forEach(task => {
        task.get('dataOutputAssociations').forEach(dataAssociationLink => {
          this.setNode(dataAssociationLink, flowElements);
        });
      });

      const tasksThatHaveDataInputAssociations = flowElements.filter(task => task.get('dataInputAssociations') &&
          task.get('dataInputAssociations').length > 0 && task.$type !== 'bpmn:IntermediateThrowEvent' && task.$type !== 'bpmn:EndEvent');
      tasksThatHaveDataInputAssociations.forEach(task => {
        task.get('dataInputAssociations').forEach(dataAssociationLink => {
          this.setNode(dataAssociationLink, flowElements);
        });
      });
    },
    loadArtifacts(flowElements, artifacts) {
      artifacts
        .filter(definition => definition.$type !== 'bpmn:Association')
        .forEach(definition => this.setNode(definition, flowElements, artifacts));
    },
    loadSequenceFlows(flowElements, artifacts) {
      flowElements
        .filter(definition => definition.$type === 'bpmn:SequenceFlow' && this.hasSourceAndTarget(definition))
        .forEach(definition => this.setNode(definition, flowElements, artifacts));
    },
    loadFlowElements(flowElements, artifacts) {
      flowElements
        .filter(definition => definition.$type !== 'bpmn:SequenceFlow')
        .forEach(definition => this.setNode(definition, flowElements, artifacts));
    },
    addLanes(process) {
      if (process.get('laneSets')[0]) {
        process.laneSets[0].lanes.forEach(this.setNode);
      }
    },
    addPools() {
      if (!this.collaboration) {
        return;
      }
      this.collaboration.get('participants').forEach(this.setNode);
    },
    setUpDiagram() {
      this.processes.forEach(process => {
        this.ensureCancelActivityIsAddedToBoundaryEvents(process);

        this.addLanes(process);

        const flowElements = process.get('flowElements');
        const artifacts = process.get('artifacts');

        this.loadFlowElements(flowElements, artifacts);
        this.loadSequenceFlows(flowElements, artifacts);
        this.loadArtifacts(flowElements, artifacts);
        this.loadAssociations(flowElements, artifacts);
        this.loadDataAssociations(flowElements);
      });

      store.commit('setRootElements', this.definitions.rootElements);

      this.addMessageFlows();

      store.commit('highlightNode', this.processNode);
    },
    getCollaboration() {
      return this.definitions.rootElements.find(({ $type }) => $type === 'bpmn:Collaboration');
    },
    getProcesses() {
      return this.definitions.rootElements.filter(({ $type }) => $type === 'bpmn:Process');
    },
    getPlane() {
      return this.definitions.diagrams[0].plane;
    },
    getPlaneElements() {
      return this.plane.get('planeElement');
    },
    parse() {
      this.collaboration = this.getCollaboration();
      this.processes = this.getProcesses();
      this.plane = this.getPlane();
      this.planeElements = this.getPlaneElements();

      this.validatePlaneElements();
      this.cleanPlaneElements();

      this.processNode = new Node(
        'processmaker-modeler-process',
        this.processes[0],
        this.planeElements.find(diagram => diagram.bpmnElement.id === this.processes[0].id),
      );
    },
    removeUnsupportedElementAttributes(definition) {
      const unsupportedElements = ['extensionElements'];

      unsupportedElements.filter(name => definition.get(name))
        .forEach(name => definition.set(name, undefined));
    },
    getCustomParser(definition) {
      const parsers = this.parsers[(definition.$type)];

      if (!parsers) {
        return;
      }

      const customParser = parsers.custom.find(parser => parser(definition, this.moddle));
      const implementationParser = parsers.implementation.find(parser => parser(definition, this.moddle));
      const defaultParser = parsers.default.find(parser => parser(definition, this.moddle));

      return customParser || implementationParser || defaultParser;
    },
    handleUnsupportedElement(bpmnType, flowElements, definition, artifacts, diagram) {
      this.addWarning({
        title: this.$t('Unsupported Element'),
        text: bpmnType + this.$t(' is an unsupported element type in parse'),
      });

      pull(flowElements, definition);
      pull(artifacts, definition);
      pull(this.planeElements, diagram);
      if (this.collaboration) {
        pull(this.collaboration.get('messageFlows'), definition);
      }

      const incomingFlows = definition.get('incoming');
      if (incomingFlows) {
        pull(flowElements, incomingFlows);
      }

      const outgoingFlows = definition.get('outgoing');
      if (outgoingFlows) {
        pull(flowElements, outgoingFlows);
      }
    },
    setNode(definition, flowElements, artifacts) {
      const diagram = this.planeElements.find(diagram => diagram.bpmnElement.id === definition.id);
      const bpmnType = definition.$type;
      const parser = this.getCustomParser(definition);

      if (!parser) {
        this.handleUnsupportedElement(bpmnType, flowElements, definition, artifacts, diagram);
        return;
      }

      this.removeUnsupportedElementAttributes(definition);

      const config = definition.config && isJSON(definition.config) ? JSON.parse(definition.config) : {};
      const type = config?.processKey || parser(definition, this.moddle);

      const unnamedElements = ['bpmn:TextAnnotation', 'bpmn:Association', 'bpmn:DataOutputAssociation', 'bpmn:DataInputAssociation'];
      const requireName = unnamedElements.indexOf(bpmnType) === -1;
      if (requireName && !definition.get('name')) {
        definition.set('name', '');
      }

      this.createNodeAsync(type, definition, diagram);
    },
    async createNodeAsync(type, definition, diagram) {
      const node =  this.createNode(type, definition, diagram);
      if (!this.isMultiplayer) {
        store.commit('addNode', node);
      }
      else {
        this.loadNodeForMultiplayer(node);
      }
    },
    async loadNodeForMultiplayer(node) {
      if (node.type === 'processmaker-modeler-lane') {
        await this.addNode(node, node.definition.id, true);
        this.nodeIdGenerator.updateCounters();
        await this.$nextTick();
        await this.paperManager.awaitScheduledUpdates();
        window.ProcessMaker.EventBus.$emit('multiplayer-addLanes', [node]);
        return;
      }
      this.multiplayerHook(node, false);
      store.commit('addNode', node);
      this.poolTarget = null;
    },
    createNode(type, definition, diagram) {
      if (Node.isTimerType(type)) {
        return new TimerEventNode(type, definition, diagram);
      }
      // Remove undefined or null properties
      Object.keys(definition).forEach(key => {
        if (definition[key] === undefined || definition[key] === null) {
          delete definition[key];
        }
      });
      return new Node(type, definition, diagram);
    },
    hasSourceAndTarget(definition) {
      const hasSource = definition.sourceRef && this.parsers[definition.sourceRef.$type];
      const hasTarget = definition.targetRef && this.parsers[definition.targetRef.$type];

      return hasSource && hasTarget;
    },
    async waitForCursorToChange() {
      const cursorWaitTime = 300;
      await this.$nextTick();
      return new Promise(resolve => setTimeout(resolve, cursorWaitTime));
    },
    async renderPaper() {
      this.isRendering = true;
      await this.paperManager.performAtomicAction(async() => {
        await this.waitForCursorToChange();
        this.parse();
        this.addPools();
        this.setUpDiagram();
      });
      await this.paperManager.awaitScheduledUpdates();
      this.isRendering = false;
      this.$emit('parsed');
    },
    async loadXML(xml = null) {
      let emitChangeEvent = false;
      if (xml === null) {
        xml = this.currentXML;
        emitChangeEvent = true;
      }
      this.definitions = await this.xmlManager.getDefinitionsFromXml(xml);
      // Migrate the definitions to new format
      this.migrateDefinitions(this.definitions);
      this.xmlManager.definitions = this.definitions;
      this.nodeIdGenerator = getNodeIdGenerator(this.definitions);
      store.commit('clearNodes');
      await this.renderPaper();
      if (emitChangeEvent) {
        window.ProcessMaker.EventBus.$emit('modeler-change');
      }
      window.ProcessMaker.EventBus.$emit('modeler-xml-loaded');
    },
    /**
     * Migrate the definitions to the new format
     *
     * @param {Object} definitions - The definitions to migrate
     */
    migrateDefinitions(definitions) {
      this.migrateInterstitialToElementDestination(definitions);
    },
    /**
     * Migrate the interstitial property to element destination property
     *
     * @param {Object} definitions - The definitions to migrate
     */
    migrateInterstitialToElementDestination(definitions) {
      // find tasks with pm:allowInterstitial="true" and change them to pm:elementDestination="{&#34;type&#34;:&#34;displayNextAssignedTask&#34;}"
      definitions.rootElements.forEach(rootElement => {
        // Check if the root element is a process
        if (rootElement.$type === 'bpmn:Process') {
          // Get all flow elements in the process
          const flowElements = rootElement.get('flowElements') || [];

          // Find all tasks with interstitial enabled
          flowElements.forEach(element => {
            if (element.$type === 'bpmn:Task' || element.$type === 'bpmn:UserTask' || element.$type === 'bpmn:ManualTask') {
              let hasConditionalRedirect = false;

              try {
                const conditionalRedirect = JSON.parse(element.get('pm:conditionalRedirect'));

                if (conditionalRedirect?.isEnabled === true && Array.isArray(conditionalRedirect.conditions)) {
                  hasConditionalRedirect = conditionalRedirect.conditions.some(
                    condition => condition?.taskDestination?.value === 'displayNextAssignedTask',
                  );
                }
              } catch (error) {
                // console.error('Error parsing conditional redirect', error);
              }

              if (element.get('pm:allowInterstitial') === true && !hasConditionalRedirect) {
                let elementDestination = element.get('pm:elementDestination');
                try {
                  elementDestination = JSON.parse(elementDestination);
                } catch (error) {
                  // console.error('Error parsing element destination', error);
                }

                if (elementDestination === undefined || elementDestination?.type !== 'displayNextAssignedTask') {
                  // Always update the element destination to display the next assigned task
                  element.set('pm:elementDestination', JSON.stringify({ type: 'displayNextAssignedTask' }));
                }
              }
            }
          });
        }
      });
    },
    getBoundaryEvents(process) {
      return process.get('flowElements').filter(({ $type }) => $type === 'bpmn:BoundaryEvent');
    },
    createBoundaryEvent(definition) {
      const boundaryEvent = boundaryEventConfig.definition(this.moddle, this.$t);
      boundaryEvent.set('id', definition.get('id'));
      boundaryEvent.set('name', definition.get('name'));
      boundaryEvent.set('eventDefinitions', definition.get('eventDefinitions'));
      boundaryEvent.set('cancelActivity', definition.get('cancelActivity'));
      boundaryEvent.set('attachedToRef', definition.get('attachedToRef'));
      boundaryEvent.set('color', definition.get('color'));
      boundaryEvent.$parent = definition.$parent;
      if (definition.get('outgoing').length > 0) {
        boundaryEvent.set('outgoing', definition.get('outgoing'));
      }
      return boundaryEvent;
    },
    ensureCancelActivityIsAddedToBoundaryEvents(process) {
      this.getBoundaryEvents(process).forEach(definition => {
        const boundaryEvent = this.createBoundaryEvent(definition);
        definition.get('outgoing').forEach(outgoing => outgoing.set('sourceRef', boundaryEvent));
        this.replaceDefinition(definition, boundaryEvent, process);
      });
    },
    replaceDefinition(definition, boundaryEvent, process) {
      const definitionIndex = process.get('flowElements').indexOf(definition);
      process.flowElements[definitionIndex] = boundaryEvent;
      const boundaryEventDiagram = this.planeElements.find((diagram) => {
        return diagram.bpmnElement === definition;
      });
      boundaryEventDiagram.bpmnElement = boundaryEvent;
    },
    toXML(cb) {
      this.moddle.toXML(this.definitions, { format: true }, cb);
    },
    onCreateElementHandler({ event, control }) {
      this.handleDrop({
        clientX: event.clientX,
        clientY: event.clientY,
        control,
      });
    },
    async addRemoteNode(data){
      const definition = this.nodeRegistry[data.type].definition(this.moddle, this.$t);
      const diagram = this.nodeRegistry[data.type].diagram(this.moddle);
      diagram.bounds.x = data.x;
      diagram.bounds.y = data.y;
      diagram.bounds.width = data.width;
      diagram.bounds.height = data.height;
      const newNode = this.createNode(data.type, definition, diagram);
      //verify if the node has a pool as a container
      if (data.poolId) {
        const pool = this.getElementByNodeId(data.poolId);
        this.poolTarget = pool;
      }
      await this.addNode(newNode, data.id, true);
      await this.$nextTick();
      await this.paperManager.awaitScheduledUpdates();
      if (this.autoValidate) {
        this.safeRevalidate();
      }
    },

    async handleDrop(data) {
      const { clientX, clientY, control, nodeThatWillBeReplaced } = data;
      this.validateDropTarget({ clientX, clientY, control });
      if (!this.allowDrop) {
        return;
      }

      const definition = this.nodeRegistry[control.type].definition(this.moddle, this.$t);

      const diagram = this.nodeRegistry[control.type].diagram(this.moddle);

      const { x, y } = this.paperManager.clientToGridPoint(clientX, clientY);
      diagram.bounds.x = x;
      diagram.bounds.y = y;

      const newNode = this.createNode(control.type, definition, diagram);
      if (newNode.isBpmnType('bpmn:BoundaryEvent')) {
        this.setShapeCenterUnderCursor(diagram);
      }

      this.$emit('before-node-added', newNode);

      this.highlightNode(newNode);

      await this.addNode(newNode);

      this.$emit('node-added', newNode);

      if (!nodeThatWillBeReplaced) {
        return;
      }

      const nodeMigrator = new NodeMigrator(
        nodeThatWillBeReplaced,
        definition,
        this.graph,
        newNode,
        this.processes,
        this.collaboration,
      );
      nodeMigrator.migrate();

      return newNode;
    },

    setShapeCenterUnderCursor(diagram) {
      diagram.bounds.x -= (diagram.bounds.width / 2);
      diagram.bounds.y -= (diagram.bounds.height / 2);
    },
    async selectNewNode(node) {
      await this.$nextTick();
      await this.paperManager.awaitScheduledUpdates();
      const newNodeComponent = this.$refs.nodeComponent.find(component => component.node === node);
      const view = newNodeComponent.shapeView;
      await this.$refs.selector.selectElement(view);
    },
    multiplayerHook(node, fromClient, isProcessRequested = false) {
      const blackList = [
        'processmaker-modeler-lane',
        'processmaker-modeler-generic-flow',
        'processmaker-modeler-sequence-flow',
        'processmaker-modeler-association',
        'processmaker-modeler-data-input-association',
        ...this.boundaryEventTypes,
      ];
      if (!this.isMultiplayer) {
        return;
      }

      if (!fromClient) {
        if (!blackList.includes(node.type) && !this.flowTypes.includes(node.type)) {
          const defaultData = {
            x: node.diagram.bounds.x,
            y: node.diagram.bounds.y,
            height: node.diagram.bounds.height,
            width: node.diagram.bounds.width,
            type: node.type,
            id: node.definition.id,
            oldNodeId: null,
            isAddingLaneAbove: true,
            color: node.definition.color,
            dueIn: node.definition.dueIn,
            name: node.definition.name,
            assignedUsers: node.definition.assignedUsers,
            config: node.definition.config,
            loopCharacteristics: null,
            gatewayDirection: null,
            messageRef: null,
            signalRef: null,
            signalPayload: null,
            extras: {},
            default: null,
          };
          if (node?.pool?.component) {
            defaultData['poolId'] = node.pool.component.id;
          }

          if (isProcessRequested) {
            return defaultData;
          }

          window.ProcessMaker.EventBus.$emit('multiplayer-addNode', defaultData);
        }
        if (this.flowTypes.includes(node.type)) {
          let sourceRefId = node.definition.sourceRef?.id;
          let targetRefId = node.definition.targetRef?.id;

          if (node.type === 'processmaker-modeler-data-input-association') {
            sourceRefId = Array.isArray(node.definition.sourceRef) && node.definition.sourceRef[0]?.id;
            targetRefId = node.definition.targetRef?.$parent?.$parent?.get('id');
          }
          const waypoint = [];

          node.diagram.waypoint?.forEach(point => {
            waypoint.push({
              x: point.x,
              y: point.y,
            });
          });

          if (sourceRefId && targetRefId) {
            const flowData = {
              id: node.definition.id,
              type: node.type,
              sourceRefId,
              targetRefId,
              waypoint,
              name: node.definition.name,
              conditionExpression: null,
              color: null,
            };

            if (isProcessRequested) {
              return flowData;
            }

            window.ProcessMaker.EventBus.$emit('multiplayer-addFlow', flowData);
          }
        }
      }
    },
    async addNode(node, id = null, fromClient = false) {
      if (!node.pool) {
        node.pool = this.poolTarget;
      }

      const targetProcess = node.getTargetProcess(this.processes, this.processNode);
      addNodeToProcess(node, targetProcess);
      node.setIds(this.nodeIdGenerator, id);

      this.planeElements.push(node.diagram);
      // add multiplayer logic as a hook
      this.multiplayerHook(node, fromClient);

      store.commit('addNode', node);
      this.poolTarget = null;

      // add processmaker-modeler-generic-flow
      if ([
        sequenceFlowId,
        laneId,
        associationId,
        messageFlowId,
        dataOutputAssociationFlowId,
        dataInputAssociationFlowId,
        genericFlowId,
      ].includes(node.type)) {
        return;
      }
      if (!fromClient) {
        // Select the node after it has been added to the store (does not apply to flows)
        this.selectNewNode(node);
      }

      return new Promise(resolve => {
        setTimeout(() => {
          this.pushToUndoStack();
          resolve();
        });
      });
    },
    async addClonedNodes(nodes) {
      const flowNodes = [];

      nodes.forEach(node => {
        if (!node.pool) {
          node.pool = this.poolTarget;
        }

        const targetProcess = node.getTargetProcess(this.processes, this.processNode);
        addNodeToProcess(node, targetProcess);

        this.planeElements.push(node.diagram);

        if (this.flowTypes.includes(node.type)) {
          // Add flow to array to render after
          flowNodes.push(node);
        } else if (this.boundaryEventTypes.includes(node.type)) {
          // Get boundary event data
          const defaultData = getBoundaryEventData(node);
          window.ProcessMaker.EventBus.$emit('multiplayer-addBoundaryEvent', defaultData);
        } else {
          this.multiplayerHook(node, false);
        }

        store.commit('addNode', node);
        this.poolTarget = null;
      });

      // Render flows after all nodes have been added
      flowNodes.forEach(node => this.multiplayerHook(node, false));
    },
    async removeNode(node, options) {
      // Check if the node is not replaced
      if (this.isMultiplayer) {
        // Emit event to server to remove node
        window.ProcessMaker.EventBus.$emit('multiplayer-removeNode', node);
      }
      this.removeNodeProcedure(node, options);

      this.$emit('removeNode', node, options);
    },
    async removeNodeProcedure(node, { removeRelationships = true } = {}) {
      if (!node) {
        // already removed
        return;
      }
      if (removeRelationships) {

        removeNodeFlows(node, this);
        removeNodeMessageFlows(node, this);
        removeNodeAssociations(node, this);
      }
      removeOutgoingAndIncomingRefsToFlow(node);
      removeBoundaryEvents(this.graph, node, this.removeNode);
      removeSourceDefault(node);

      this.removeNodesFromLane(node);
      this.removeNodesFromPool(node);
      store.commit('removeNode', node);
      store.commit('highlightNode', this.processNode);
      this.$refs.selector.clearSelection();
      await this.$nextTick();
      await this.pushToUndoStack();
      // force to update the processNode property in every delete
      this.processes = this.getProcesses();
      if (this.processes  && this.processes.length > 0) {
        this.processNode = new Node(
          'processmaker-modeler-process',
          this.processes[0],
          this.planeElements.find(diagram => diagram.bpmnElement.id === this.processes[0].id),
        );
      }
    },
    async removeNodes() {
      await this.performSingleUndoRedoTransaction(async() => {
        await this.paperManager.performAtomicAction(async() => {
          const waitPromises = [];
          this.highlightedNodes.forEach((node) =>
            waitPromises.push(this.removeNode(node, { removeRelationships: true })),
          );
          await Promise.all(waitPromises);
          store.commit('highlightNode');
        });
      });
    },
    replaceNode({ node, typeToReplaceWith }) {
      this.performSingleUndoRedoTransaction(async() => {
        await this.paperManager.performAtomicAction(async() => {
          const { x, y } = node.diagram.bounds;

          const nodeData = {
            x, y,
            control: { type: typeToReplaceWith },
            nodeThatWillBeReplaced: node,
          };
          await this.replaceNodeProcedure(nodeData, true);
        });
      });
    },
    async replaceNodeProcedure(data, isReplaced = false) {
      if (isReplaced) {
        // Get the clientX and clientY from the node that will be replaced
        const { x: clientX, y: clientY } = this.paper.localToClientPoint(data.nodeThatWillBeReplaced.diagram.bounds);
        data.clientX = clientX;
        data.clientY = clientY;
      }
      await this.handleDrop(data);
      await this.removeNode(data.nodeThatWillBeReplaced, { removeRelationships: false, isReplaced });
    },
    replaceAiNode({ node, typeToReplaceWith, assetId, assetName, redirectTo }) {
      this.performSingleUndoRedoTransaction(async() => {
        await this.paperManager.performAtomicAction(async() => {
          const { x: clientX, y: clientY } = this.paper.localToClientPoint(node.diagram.bounds);
          const newNode = await this.handleDrop({
            clientX, clientY,
            control: { type: typeToReplaceWith },
            nodeThatWillBeReplaced: node,
          });

          if (typeToReplaceWith === 'processmaker-modeler-task') {
            newNode.definition.screenRef = assetId;
            newNode.definition.name = assetName;
          }

          if (typeToReplaceWith === 'processmaker-modeler-script-task') {
            newNode.definition.scriptRef = assetId;
            newNode.definition.name = assetName;
          }

          if (typeToReplaceWith === 'processmaker-modeler-call-activity') {
            newNode.definition.name = assetName;
            newNode.definition.calledElement = `ProcessId-${assetId}`;
            newNode.definition.config = `{"calledElement":"ProcessId-${assetId}","processId":${assetId},"startEvent":"node_1","name":"${assetName}"}`;
            redirectTo = `${redirectTo}/${newNode.id}`;
          }

          await this.removeNode(node, { removeRelationships: false });
          this.highlightNode(newNode);
          this.selectNewNode(newNode);
          await this.pushToUndoStack();
          await this.$nextTick();
          this.saveBpmn(redirectTo, newNode.id);
        });
      });
    },
    replaceGenericFlow({ actualFlow, genericFlow, targetNode }) {
      this.performSingleUndoRedoTransaction(async() => {
        await this.paperManager.performAtomicAction(async() => {
          await this.highlightNode(null);
          await this.$nextTick();
          await store.commit('removeNode', genericFlow);
          await this.$nextTick();
          await this.addNode(actualFlow, genericFlow.definition.id);
          await this.$nextTick();
          await this.highlightNode(targetNode);
        });
      });
    },
    async performSingleUndoRedoTransaction(cb) {
      undoRedoStore.commit('disableSavingState');
      await cb();
      undoRedoStore.commit('enableSavingState');
      await this.pushToUndoStack();
    },
    removeNodesFromLane(node) {
      const containingLane = node.pool && node.pool.component.laneSet &&
        node.pool.component.laneSet.get('lanes').find(lane => {
          return lane.get('flowNodeRef').includes(node.definition);
        });

      if (!containingLane) {
        return;
      }

      pull(containingLane.get('flowNodeRef'), node.definition);
    },
    removeNodesFromPool(node) {
      if (node.type === 'processmaker-modeler-pool' && node.definition.processRef) {
        if (node.definition.processRef.artifacts) {
          node.definition.processRef.artifacts.forEach(artifact => {
            const nodeToRemove = this.nodes.find(n => n.definition === artifact);
            if (nodeToRemove) {
              this.removeNode(nodeToRemove);
            }
          });
        }
        if (node.definition.processRef.flowElements) {
          node.definition.processRef.flowElements.forEach(flowElement => {
            const nodeToRemove = this.nodes.find(n => n.definition === flowElement);
            if (nodeToRemove) {
              this.removeNode(nodeToRemove);
            }
          });
        }
        let laneSets = node.definition.processRef.laneSets;
        if (!!laneSets && laneSets.length > 0) {
          laneSets[0].lanes.forEach(lane => {
            const nodeToRemove = this.nodes.find(n => n.definition === lane);
            if (nodeToRemove) {
              this.removeNode(nodeToRemove);
            }
          });
        }
      }
    },
    handleResize() {
      const { clientWidth, clientHeight } = this.$el.parentElement;
      this.parentWidth = clientWidth + 'px';
      this.parentHeight = clientHeight + 'px';

      this.paperManager.setPaperDimensions(clientWidth, clientHeight);
    },
    validateDropTarget({ clientX, clientY, control }) {
      const { allowDrop, poolTarget } = getValidationProperties(clientX, clientY, control, this.paperManager.paper, this.graph, this.collaboration, this.$refs['paper-container']);
      this.allowDrop = allowDrop;
      this.poolTarget = poolTarget;
    },
    isBpmnNode(shape) {
      return shape.component != null;
    },
    setShapeStacking(shape) {
      if (this.isRendering || (!shape.component.node.isType('processmaker-modeler-pool') && shape === this.previouslyStackedShape)) {
        return;
      }

      this.previouslyStackedShape = shape;
      this.paperManager.performAtomicAction(() => ensureShapeIsNotCovered(shape, this.graph));
    },
    showSavedNotification() {
      undoRedoStore.dispatch('saved');
    },
    enableVersions() {
      undoRedoStore.dispatch('enableVersions');
    },
    setVersionIndicator(isDraft) {
      undoRedoStore.dispatch('setVersionIndicator', isDraft);
    },
    setLoadingState(isLoading) {
      undoRedoStore.dispatch('setLoadingState', isLoading);
    },
    clearSelection(){
      this.$refs.selector.clearSelection();
    },
    isPointInSelection(event) {
      const selector = this.$refs.selector.$el;
      if (typeof selector.getBoundingClientRect === 'function') {
        // check if mouse was clicked inside the selector
        const { x: sx, y: sy, width:swidth, height: sheight } = selector.getBoundingClientRect();
        if (event.clientX >= sx && event.clientX <= sx + swidth && event.clientY >= sy && event.clientY <= sy + sheight) {
          return true;
        }
      }
      return false;
    },
    async pointerDowInShape(event, element) {
      const { clientX: x, clientY: y } = event;
      const shapeView = this.paper.findViewByModel(element);
      this.isDragging = false;
      this.isSelecting = false;
      this.isIntoTheSelection = false;
      this.dragStart = { x, y };
      // Verify if is in the selection box
      if (this.isPointInSelection(event)) {
        this.isIntoTheSelection = true;
      } else {
        if (!event.shiftKey) {
          await this.$refs.selector.selectElement(shapeView);
          await this.$nextTick();
        }
      }
      this.$refs.selector.startDrag({
        clientX: event.clientX,
        clientY: event.clientY,
      });
    },
    pointerDownHandler(event) {
      const { clientX: x, clientY: y } = event;
      this.isDragging = false;
      this.isSelecting = false;
      this.isIntoTheSelection = false;
      this.dragStart = { x, y };
      // Verify if is in the selection box
      if (this.isPointInSelection(event)) {
        this.isIntoTheSelection = true;
        this.$refs.selector.startDrag({
          clientX: event.clientX,
          clientY: event.clientY,
        });
      } else {
        this.isSelecting = true;
        this.$refs.selector.startSelection(event);
      }
    },
    pointerMoveHandler(event) {
      const { clientX: x, clientY: y } = event;
      const { paper } = this.paperManager;
      let updateMousePosition = _.debounce(function() {
        window.ProcessMaker.EventBus.$emit('multiplayer-updateMousePosition', {
          coordinates: {
            clientX: x,
            clientY: y,
          },
          paperTranslate:  paper.translate(), // add papper translate
          paperScale: paper.scale(), // add scale
        });
      }, 3000000, { leading: true, trailing: true });
      updateMousePosition();

      if (this.panMode || this.isForDocumenting) {
        return;
      }

      if (this.dragStart && (Math.abs(x - this.dragStart.x) > 5 || Math.abs(y - this.dragStart.y) > 5)) {
        this.isDragging = true;
        this.dragStart = null;
      } else {
        if (this.isSelecting) {
          this.$refs.selector.updateSelection(event);
        } else {
          if (this.isDragging) {
            this.$refs.selector.drag(event);
          }
        }
      }
    },
    pointerUpHandler(event, cellView) {
      if (!this.isDragging && this.dragStart) {
        // is clicked over the shape
        if (cellView) {
          this.$refs.selector.selectElement(cellView, event.shiftKey);
        } else {
          this.clearSelection();
        }
      } else {
        if (this.isSelecting) {
          this.$refs.selector.endSelection(this.paperManager.paper);
        } else {
          this.$refs.selector.stopDrag();
        }
      }
      window.ProcessMaker.EventBus.$emit('custom-pointerclick', event);
      this.isDragging = false;
      this.dragStart = null;
      this.isSelecting = false;
    },
    redirect(redirectTo) {
      if (window.ProcessMaker.AbTesting) {
        window.parent.location = redirectTo;
      } else {
        window.location = redirectTo;
      }
    },
    enableMultiplayer(value) {
      store.commit('enableMultiplayer', value);
    },
    emptyPlayers(){
      this.players = [];
    },
    addPlayer(data) {
      const player = this.players.find(player => player.id === data.id);
      if (!player) {
        this.players.push(data);
      }
    },
    /**
     * Update Client Cursor
     * @param {Object} data
     */
    updateClientCursor(data) {
      if (data) {
        this.players = this.players.map((item) => (item.id === data.id ? { ...item, ...data } : item));
      }
    },
    /**
     * Prepare cursor data taking into account, paper translate and paper scale
     * @param {Object} player
     * @return {Object}
     */
    prepareCursorData(player) {
      const { paper } = this.paperManager;
      const localTranslate = paper.translate();
      // calculate paper translated deltas
      let deltaX = localTranslate.tx - player.cursor.paperTranslate.tx/player.cursor.paperScale.sx;
      let deltaY = localTranslate.ty - player.cursor.paperTranslate.ty/player.cursor.paperScale.sy;
      // get new cursor coordinates taking into consideration paper scale and paper translated
      const left = player.cursor.coordinates.clientX/player.cursor.paperScale.sx + deltaX;
      const top = player.cursor.coordinates.clientY/player.cursor.paperScale.sy + deltaY;
      return {
        cursor: { left, top },
        color: player.color,
        name: player.name,
      };
    },
    /**
     * Unhightligt selected Nodes
     * @param {String} clientId
     */
    unhightligtNodes(clientId) {
      const player = this.players.find(player => player.id === clientId);

      player?.selectedNodes?.forEach((nodeId) => {
        const element = this.getElementByNodeId(nodeId);
        if (element) {
          element.component.setHighlightColor(false, player.color);
        }
      });
    },
    /**
     * Update the hightligted nodes
     * @param {Object} data
     */
    updateHightligtedNodes(data) {
      if (data) {
        this.unhightligtNodes(data.id);
        // highlight selected shape
        this.players = this.players.map((item) => (item.id === data.id ? { ...item, ...data } : item));
        data?.selectedNodes?.forEach((nodeId) => {
          const element = this.getElementByNodeId(nodeId);
          if (element) {
            element.component.setHighlightColor(true, data.color);
          }
        });
      }
    },
    isMultiplayerSelected(data) {
      let intersectionExists = false;
      if (data) {
        this.players?.some((player) => {
          if (intersectionExists) {
            return true; // This will break out of the loop
          }
          intersectionExists = player?.selectedNodes?.some(item => data.includes(item));
          return false;
        });

      }
      return intersectionExists;
    },
    removePlayer(playerId) {
      const playerIndex = this.players.findIndex(player => player.id === playerId);

      if (playerIndex !== -1) {
        this.players.splice(playerIndex, 1);
      }
    },
    /**
     * Update the lasso tool
     */
    updateLasso(){
      this.$refs.selector.updateSelectionBox();
    },
    getNonce() {
      const max = 999999999999999;
      const nonce = Math.floor(Math.random() * max);
      this.currentNonce = nonce;
      localStorage.currentNonce = this.currentNonce;
    },
    getPromptSessionForUser() {
      // Get sessions list
      let promptSessions = localStorage.getItem('promptSessions');

      // If promptSessions does not exist, set it as an empty array
      promptSessions = promptSessions ? JSON.parse(promptSessions) : [];
      let item = promptSessions.find(item => item.userId === window.ProcessMaker?.modeler?.process?.user_id && item.server === window.location.host);

      if (item) {
        return item.promptSessionId;
      }

      return '';
    },
    setPromptSessions(promptSessionId) {
      let index = 'userId';
      let id = window.ProcessMaker?.modeler?.process?.user_id;

      // Get sessions list
      let promptSessions = localStorage.getItem('promptSessions');

      // If promptSessions does not exist, set it as an empty array
      promptSessions = promptSessions ? JSON.parse(promptSessions) : [];

      let item = promptSessions.find(item => item[index] === id && item.server === window.location.host);

      if (item) {
        item.promptSessionId = promptSessionId;
      } else {
        promptSessions.push({ [index]: id, server: window.location.host, promptSessionId });
      }

      localStorage.setItem('promptSessions', JSON.stringify(promptSessions));
    },
    removePromptSessionForUser() {
      // Get sessions list
      let promptSessions = localStorage.getItem('promptSessions');

      // If promptSessions does not exist, set it as an empty array
      promptSessions = promptSessions ? JSON.parse(promptSessions) : [];

      let item = promptSessions.find(item => item.userId === window.ProcessMaker?.modeler?.process?.user_id && item.server === window.location.host);

      if (item) {
        item.promptSessionId = '';
      }

      localStorage.setItem('promptSessions', JSON.stringify(promptSessions));
    },
    fetchHistory() {
      let url = '/package-ai/getPromptSessionHistory';

      let params = {
        server: window.location.host,
        processId: window.ProcessMaker?.modeler?.process?.id,
      };

      if (this.promptSessionId && this.promptSessionId !== null && this.promptSessionId !== '') {
        params = {
          promptSessionId: this.promptSessionId,
        };
      }

      window.ProcessMaker.apiClient.post(url, params)
        .then(response => {
          this.setPromptSessions((response.data.promptSessionId));
          this.promptSessionId = (response.data.promptSessionId);
          localStorage.promptSessionId = (response.data.promptSessionId);
        })
        .catch((error) => {
          const errorMsg = error.response?.data?.message || error.message;

          this.loading = false;
          if (error.response.status === 404) {
            this.removePromptSessionForUser();
            localStorage.promptSessionId = '';
            this.promptSessionId = '';
            this.fetchHistory();
          } else {
            console.error(errorMsg, 'danger');
          }
        });
    },
    onGenerateAssets() {
      this.isAiGenerated = false;
      this.saveBpmn(null, null, true);
    },
    generateAssets() {
      this.getNonce();

      // Show Generating message
      this.loadingAI = true;

      this.fetchHistory();

      const params = {
        promptSessionId: this.promptSessionId,
        nonce: this.currentNonce,
        processId: window.ProcessMaker?.modeler?.process?.id,
        alternative: window.ProcessMaker?.AbTesting?.alternative || 'A',
      };

      const url = '/package-ai/generateProcessArtifacts';

      window.ProcessMaker.apiClient.post(url, params)
        .then((response) => {
          if (response.data) {
            if (response.data?.error) {
              this.assetFail = true;
              this.loadingAI = false;
            }
          }
        })
        .catch(() => {
          const errorMsg = this.$t('ProcessMaker AI is currently offline. Please try again later.');
          window.ProcessMaker.alert(errorMsg, 'danger');
          this.assetFail = true;
          this.loadingAI = false;
        });
    },
    highlightTaskArrays(data) {
      if (data) {
        this.addAiHighlights(data.tasks);
        this.addAiHighlights(data.serviceTasks);
        this.addAiHighlights(data.scriptTasks);
      }
    },
    unhighlightTaskArrays(data) {
      if (data) {
        this.removeAiHighlights(data.tasks);
        this.removeAiHighlights(data.serviceTasks);
        this.removeAiHighlights(data.scriptTasks);
      }
    },
    addAiHighlights(taskArray) {
      let self = this;
      taskArray.forEach(task => {
        const taskNode = self.getElementByNodeId(task.id);
        if (taskNode) {
          taskNode.component.setAiStatusHighlight(task.status);
        }
      });
    },
    removeAiHighlights(taskArray) {
      let self = this;
      taskArray.forEach(task => {
        const taskNode = self.getElementByNodeId(task.id);
        if (taskNode) {
          taskNode.component.unsetHighlights();
        }
      });
    },
    subscribeToProgress() {
      const channel = `ProcessMaker.Models.User.${window.ProcessMaker?.user?.id}`;
      const streamProgressEvent = '.ProcessMaker\\Package\\PackageAi\\Events\\GenerateArtifactsProgressEvent';
      if (!window.Echo) {
        return;
      }
      window.Echo.private(channel).listen(
        streamProgressEvent,
        (response) => {
          const alternative = window.ProcessMaker.AbTesting?.alternative || 'A';
          if (this.shouldOmitEvent(response, alternative, true)) {
            return;
          }

          if (response.data) {
            if (response.data.progress.status === 'running') {
              this.loadingAI = true;
              this.highlightTaskArrays(response.data);
            } else if (response.data.progress.status === 'error') {
              this.loadingAI = false;
              window.ProcessMaker.alert(response.data.message, 'danger');
              // Stop and show error
              this.assetFail = true;
              this.loadingAI = false;
            } else {
              this.highlightTaskArrays(response.data);
              setTimeout(() => {
                this.unhighlightTaskArrays(response.data);
              }, 1000);
              this.setPromptSessions(response.data.promptSessionId);
              // Successful generation
              this.assetsCreated = true;
              this.fetchHistory();
              setTimeout(() => {
                this.loadingAI = false;
              }, 500);
            }
          }
        },
      );
    },
    shouldOmitEvent(response, alternative, unhighlightTasks = false) {
      if (response.data.alternative !== alternative) {
        return true;
      }

      if (parseInt(response.data.processId) !== parseInt(window.ProcessMaker?.modeler?.process?.id)) {
        return true;
      }

      if (response.data.promptSessionId !== this.promptSessionId) {
        if (unhighlightTasks) {
          this.unhighlightTaskArrays(response.data);
        }
        return true;
      }

      if (this.cancelledJobs.some((element) => element === parseInt(response.data.nonce))) {
        if (unhighlightTasks) {
          this.unhighlightTaskArrays(response.data);
        }
        return true;
      }
      return false;
    },
    subscribeToGenerationCompleted() {
      const channel = `ProcessMaker.Models.User.${window.ProcessMaker?.user?.id}`;
      const streamCompletedEvent = '.ProcessMaker\\Package\\PackageAi\\Events\\GenerateArtifactsCompletedEvent';
      window.Echo.private(channel).listen(
        streamCompletedEvent,
        (response) => {
          const alternative = window.ProcessMaker.AbTesting?.alternative || 'A';
          if (this.shouldOmitEvent(response, alternative)) {
            return;
          }

          if (response.data) {
            this.updateScreenRefs(response.data.screenIds);
            this.updateScriptRefs(response.data.scriptIds);
          }
        },
      );
    },
    subscribeToErrors() {
      const channel = `ProcessMaker.Models.User.${window.ProcessMaker?.user?.id}`;
      const streamProgressEvent = '.ProcessMaker\\Package\\PackageAi\\Events\\GenerateArtifactsErrorEvent';
      window.Echo.private(channel).listen(
        streamProgressEvent,
        () => {
          // Output error
          this.assetFail = true;
          this.loadingAI = false;
        },
      );
    },
    onStopAssetGeneration() {
      if (this.currentNonce) {
        this.cancelledJobs.push(this.currentNonce);
        localStorage.setItem('cancelledJobs', JSON.stringify(this.cancelledJobs));
        this.loadingAI = false;
      }
    },
    updateScreenRefs(elements) {
      elements.forEach(el => {
        const node = this.nodes.find(n => n.definition.id === el.node_id);

        if (!node) {
          return;
        }

        if (node.type === 'processmaker-modeler-task') {
          store.commit('updateNodeProp', { node, key: 'screenRef', value: el.screen_id });
          this.$emit('save-state');
        }
      });
    },
    updateScriptRefs(elements) {
      elements.forEach(el => {
        const node = this.nodes.find(n => n.definition.id === el.node_id);

        if (!node) {
          return;
        }

        if (node.type === 'processmaker-modeler-script-task') {
          store.commit('updateNodeProp', { node, key: 'scriptRef', value: el.script_id });
          this.$emit('save-state');
        }
      });
    },
    applyPreviewVisibilityForSelection() {
      if (this.highlightedNodes.length !== 1) {
        this.isOpenPreview = false;
      }
      else {
        const nodeType =  this.highlightedNodes[0].definition?.$type;
        this.isOpenPreview = this.isOpenPreview && this.validPreviewElements.includes(nodeType);
      }
    },
    reset(payload) {
      this.$refs.selector.clearSelection();

      store.commit('reset', payload);
    },
    adjustPaperPosition() {
      this.centered = false;
      this.paper.on('render:done scale:changed translate:changed', () => {
        if (this.isForDocumenting && !this.centered) {
          this.paperManager.centerContent();
          this.centered = true;
        }
      });
    },
    async safeRevalidate() {
      // Run bpmnlint only when auto-validate is enabled and the model is ready.
      if (!this.autoValidate) return;
      
      // Wait for Vue and JointJS to settle before validating, so we don't lose errors
      await this.$nextTick();
      if (this.paperManager?.awaitScheduledUpdates) {
        await this.paperManager.awaitScheduledUpdates();
      }
      await this.validateBpmnDiagram();
    },
  },
  created() {
    if (runningInCypressTest()) {
      /* Add reference to store on window; this is used in testing to verify rendered nodes */
      window.store = store;
      window.undoRedoStore = undoRedoStore;
    }

    this.$t = this.$t.bind(this);
    /**
     * Before Initialize the BpmnModdle and its extensions.
     * In this stage the node components were not yet registered,
     * so they could be extended.
     *
     */
    window.ProcessMaker.EventBus.$emit('modeler-before-init', {
      registerComponentMixin: this.registerComponentMixin,
    });

    this.registerNode(Process);
    /* Initialize the BpmnModdle and its extensions */
    window.ProcessMaker.EventBus.$emit('modeler-init', {
      $t: this.$t,
      modeler: this,
      registerMenuAction: this.registerMenuAction,
      registerInspectorExtension,
      registerBpmnExtension: this.registerBpmnExtension,
      registerNode: this.registerNode,
      registerStatusBar: this.registerStatusBar,
      registerPmBlock: this.registerPmBlock,
      registerPreview: this.registerPreview,
      registerCustomDropdownData: this.registerCustomDropdownData,
    });

    this.moddle = new BpmnModdle(this.extensions);
    this.linter = new Linter(linterConfig);
    this.xmlManager = new XMLManager(this.moddle);
    this.$emit('set-xml-manager', this.xmlManager);
  },
  mounted() {
    this.mountedInit();
    this.addEventHandlers();
    this.registerCustomNodes();
    this.initAI();
    this.linkEditingInit();
    this.initTransparentDragging();
    this.test = true;
    this.adjustPaperPosition();
    this.safeRevalidate();
  },
};
</script>
<style lang="scss" src="./modeler.scss" />
