<template>
    <div id="columns" class="d-flex">
        <StateUpgradeModal :state-messages="stateMessages" />
        <StateUpgradeModal
            :state-messages="insertedStateMessages"
            title="Subworkflow embedded with changes"
            message="Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save." />
        <RefactorConfirmationModal
            :workflow-id="id"
            :version="version"
            :versions="versions"
            :refactor-actions="refactorActions"
            @onWorkflowError="onWorkflowError"
            @onWorkflowMessage="onWorkflowMessage"
            @onRefactor="onRefactor"
            @onShow="hideModal" />
        <MessagesModal :title="messageTitle" :message="messageBody" :error="messageIsError" @onHidden="resetMessage" />
        <SaveChangesModal :nav-url.sync="navUrl" :show-modal.sync="showSaveChangesModal" @on-proceed="onNavigate" />
        <b-modal
            v-model="showSaveAsModal"
            title="Save As a New Workflow"
            ok-title="Save"
            cancel-title="Cancel"
            @ok="doSaveAs(false)">
            <b-form-group label="Name">
                <b-form-input v-model="saveAsName" />
            </b-form-group>
            <b-form-group label="Annotation">
                <b-form-textarea v-model="saveAsAnnotation" />
            </b-form-group>
        </b-modal>
        <ActivityBar
            ref="activityBar"
            :default-activities="workflowActivities"
            :special-activities="specialWorkflowActivities"
            :exit-activity="exitWorkflowActivity"
            activity-bar-id="workflow-editor"
            :show-admin="false"
            options-title="Options"
            options-heading="Workflow Options"
            options-tooltip="View additional workflow options"
            options-search-placeholder="Search options"
            initial-activity="workflow-editor-attributes"
            :options-icon="faCog"
            :hide-panel="reportActive"
            @activityClicked="onActivityClicked">
            <template v-slot:side-panel="{ isActiveSideBar }">
                <ToolPanel v-if="isActiveSideBar('workflow-editor-tools')" workflow @onInsertTool="onInsertTool" />
                <SearchPanel
                    v-if="isActiveSideBar('workflow-editor-search')"
                    @result-clicked="(data) => onHighlightRegion(data.bounds)" />
                <InputPanel
                    v-if="isActiveSideBar('workflow-editor-inputs')"
                    :inputs="inputs"
                    @insertModule="onInsertModule" />
                <WorkflowLint
                    v-else-if="isActiveSideBar('workflow-best-practices')"
                    :lint-data="lintData"
                    :steps="steps"
                    :datatypes-mapper="datatypesMapper"
                    :has-changes="hasChanges"
                    :on-save="onSave"
                    @onAttributes="
                        (e) => {
                            showAttributes(e);
                        }
                    "
                    @onHighlightRegion="(bounds) => onHighlightRegion(bounds, false)"
                    @onRefactor="onAttemptRefactor"
                    @onScrollTo="onScrollTo" />
                <UndoRedoStack v-else-if="isActiveSideBar('workflow-undo-redo')" :store-id="id" />
                <WorkflowPanel
                    v-else-if="isActiveSideBar('workflow-editor-workflows')"
                    :current-workflow-id="id"
                    @insertWorkflow="onInsertWorkflow"
                    @insertWorkflowSteps="onInsertWorkflowSteps"
                    @createWorkflow="createNewWorkflow" />
                <WorkflowAttributes
                    v-else-if="isActiveSideBar('workflow-editor-attributes')"
                    :id="id"
                    :tags="tags"
                    :highlight.sync="highlightAttribute"
                    :parameters="parameters"
                    :annotation="annotation"
                    :name="name"
                    :version="version"
                    :versions="versions"
                    :license="license"
                    :creator="creator"
                    :doi="doi"
                    :logo-url="logoUrl"
                    :help="help"
                    :readme-active.sync="readmeActive"
                    @version="onVersion"
                    @tags="setTags"
                    @license="onLicense"
                    @creator="onCreator"
                    @doi="onDoi"
                    @update:nameCurrent="setName"
                    @update:annotationCurrent="setAnnotation"
                    @update:logoUrlCurrent="setLogoUrl"
                    @update:helpCurrent="setHelp" />
                <UserToolPanel
                    v-if="isActiveSideBar('workflow-editor-user-defined-tools')"
                    :in-workflow-editor="true"
                    in-panel
                    @onInsertTool="onInsertTool" />
            </template>
        </ActivityBar>
        <template v-if="reportActive">
            <MarkdownEditor
                ref="markdownEditor"
                :markdown-text="report.markdown"
                mode="report"
                :title="'Workflow Report: ' + name"
                :labels="getLabels"
                :steps="steps"
                @insert="insertMarkdown"
                @update="onReportUpdate">
                <template v-slot:buttons>
                    <b-button
                        id="workflow-canvas-button"
                        v-b-tooltip.hover.bottom
                        title="Return to Workflow"
                        variant="link"
                        role="button"
                        @click="showAttributes">
                        <FontAwesomeIcon :icon="faTimes" />
                    </b-button>
                </template>
            </MarkdownEditor>
        </template>
        <template v-else>
            <div id="center" class="workflow-center">
                <div class="editor-top-bar" unselectable="on">
                    <span>
                        <span class="sr-only">Workflow Editor</span>
                        <span class="editor-title" :title="name"
                            >{{ name }}
                            <i v-if="hasChanges" class="text-muted"> (unsaved changes) </i>
                        </span>
                    </span>

                    <b-button-group>
                        <BDropdown
                            v-if="credentialSteps.length > 0"
                            no-caret
                            right
                            variant="link"
                            style="z-index: 60000"
                            title="Workflow contains steps that require credentials"
                            @show="() => (showDropdown = true)"
                            @hide="() => (showDropdown = false)">
                            <template v-slot:button-content>
                                <FontAwesomeIcon :icon="faKey" fixed-width />
                            </template>

                            <BDropdownText style="min-width: 25rem">
                                This workflow contains the following steps that require credentials:
                            </BDropdownText>

                            <BDropdownDivider />

                            <BDropdownItem
                                v-for="cs in credentialSteps"
                                :key="cs.id"
                                title="Click to go to step"
                                class="mr-0"
                                @click="onToolClick(cs.id)">
                                <FontAwesomeIcon :icon="faWrench" fixed-width />
                                {{ cs.id + 1 }}: {{ cs.label ?? cs.name }}
                            </BDropdownItem>
                        </BDropdown>

                        <b-button
                            :title="undoRedoStore.undoText + ' (Ctrl + Z)'"
                            variant="secondary"
                            :disabled="!undoRedoStore.hasUndo"
                            @click="undoRedoStore.undo()">
                            <FontAwesomeIcon :icon="faArrowLeft" />
                        </b-button>
                        <b-button
                            :title="undoRedoStore.redoText + ' (Ctrl + Shift + Z)'"
                            variant="secondary"
                            :disabled="!undoRedoStore.hasRedo"
                            @click="undoRedoStore.redo()">
                            <FontAwesomeIcon :icon="faArrowRight" />
                        </b-button>
                        <b-button
                            id="workflow-save-button"
                            class="py-1 px-2"
                            variant="link"
                            :disabled="!hasChanges"
                            :title="saveWorkflowTitle"
                            @click="saveOrCreate">
                            <FontAwesomeIcon :icon="faSave" />
                        </b-button>
                    </b-button-group>
                </div>

                <ReadmeEditor
                    v-if="readmeActive"
                    class="p-2"
                    :readme="readme"
                    :name="name"
                    :logo-url="logoUrl"
                    @exit="readmeActive = false"
                    @update:readmeCurrent="setReadme" />

                <WorkflowGraph
                    v-if="!datatypesMapperLoading"
                    v-show="!readmeActive"
                    ref="workflowGraph"
                    :steps="steps"
                    :datatypes-mapper="datatypesMapper"
                    :scroll-to-id="scrollToId"
                    :initial-position="{ x: 50, y: 50 }"
                    :loading="loadingWorkflow || initialLoading"
                    @scrollTo="scrollToId = null"
                    @transform="(value) => (transform = value)"
                    @graph-offset="(value) => (graphOffset = value)"
                    @onClone="onClone"
                    @onCreate="onInsertTool"
                    @onChange="onChange"
                    @onRemove="onRemove"
                    @onUpdateStepPosition="onUpdateStepPosition">
                    <NodeInspector
                        v-if="activeStep"
                        :step="activeStep"
                        :datatypes="datatypes"
                        @postJobActionsChanged="onChangePostJobActions"
                        @annotationChanged="onAnnotation"
                        @labelChanged="onLabel"
                        @dataChanged="onSetData"
                        @stepUpdated="updateStep"
                        @editSubworkflow="onEditSubworkflow"
                        @attemptRefactor="onAttemptRefactor"
                        @close="activeNodeId = null"></NodeInspector>
                </WorkflowGraph>
            </div>
        </template>
    </div>
</template>

<script>
import { faArrowLeft, faArrowRight, faCog, faKey, faSave, faTimes, faWrench } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { until, whenever } from "@vueuse/core";
import { logicAnd, logicNot, logicOr } from "@vueuse/math";
import { BDropdown, BDropdownDivider, BDropdownItem, BDropdownText } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import Vue, { computed, nextTick, onUnmounted, ref, unref, watch } from "vue";

import { getUntypedWorkflowParameters } from "@/components/Workflow/Editor/modules/parameters";
import { getWorkflowFull } from "@/components/Workflow/workflows.services";
import { ConfirmDialog, useConfirmDialog } from "@/composables/confirmDialog";
import { useDatatypesMapper } from "@/composables/datatypesMapper";
import { Toast } from "@/composables/toast";
import { useMagicKeys } from "@/composables/useMagicKeys";
import { useUid } from "@/composables/utils/uid";
import { provideScopedWorkflowStores } from "@/composables/workflowStores";
import { getAppRoot } from "@/onload/loadConfig";
import { useScopePointerStore } from "@/stores/scopePointerStore";
import { useUnprivilegedToolStore } from "@/stores/unprivilegedToolStore";
import { LastQueue } from "@/utils/lastQueue";
import { errorMessageAsString } from "@/utils/simple-error";
import { textify } from "@/utils/utils";

import { Services } from "../services";
import { InsertStepAction, useStepActions } from "./Actions/stepActions";
import { CopyIntoWorkflowAction, SetValueActionHandler } from "./Actions/workflowActions";
import { defaultPosition } from "./composables/useDefaultStepPosition";
import { useWorkflowBoundingBox } from "./composables/workflowBoundingBox";
import { useSpecialWorkflowActivities, useWorkflowActivities } from "./modules/activities";
import { getWorkflowInputs } from "./modules/inputs";
import { fromSteps } from "./modules/labels";
import { fromSimple } from "./modules/model";
import { getModule, getVersions, saveWorkflow } from "./modules/services";
import { useLintData } from "./modules/useLinting";
import { getStateUpgradeMessages } from "./modules/utilities";
import reportDefault from "./reportDefault";

import WorkflowLint from "./Lint.vue";
import MessagesModal from "./MessagesModal.vue";
import NodeInspector from "./NodeInspector.vue";
import ReadmeEditor from "./ReadmeEditor.vue";
import RefactorConfirmationModal from "./RefactorConfirmationModal.vue";
import SaveChangesModal from "./SaveChangesModal.vue";
import StateUpgradeModal from "./StateUpgradeModal.vue";
import WorkflowAttributes from "./WorkflowAttributes.vue";
import WorkflowGraph from "./WorkflowGraph.vue";
import ActivityBar from "@/components/ActivityBar/ActivityBar.vue";
import MarkdownEditor from "@/components/Markdown/MarkdownEditor.vue";
import InputPanel from "@/components/Panels/InputPanel.vue";
import SearchPanel from "@/components/Panels/SearchPanel.vue";
import ToolPanel from "@/components/Panels/ToolPanel.vue";
import UserToolPanel from "@/components/Panels/UserToolPanel.vue";
import WorkflowPanel from "@/components/Panels/WorkflowPanel.vue";
import UndoRedoStack from "@/components/UndoRedo/UndoRedoStack.vue";

export default {
    components: {
        ActivityBar,
        MarkdownEditor,
        SaveChangesModal,
        StateUpgradeModal,
        ReadmeEditor,
        ToolPanel,
        WorkflowAttributes,
        WorkflowLint,
        RefactorConfirmationModal,
        MessagesModal,
        WorkflowGraph,
        FontAwesomeIcon,
        UndoRedoStack,
        WorkflowPanel,
        NodeInspector,
        InputPanel,
        UserToolPanel,
        SearchPanel,
        BDropdownItem,
        BDropdown,
        BDropdownText,
        BDropdownDivider,
    },
    props: {
        workflowId: {
            type: String,
            default: undefined,
        },
        initialVersion: {
            type: Number,
            default: undefined,
        },
        workflowTags: {
            type: Array,
            default: () => [],
        },
    },
    setup(props, { emit }) {
        const { datatypes, datatypesMapper, datatypesMapperLoading } = useDatatypesMapper();

        const uid = unref(useUid("workflow-editor-"));
        const id = ref(props.workflowId || uid);

        const { connectionStore, stepStore, stateStore, commentStore, undoRedoStore } = provideScopedWorkflowStores(id);

        const { getWorkflowBoundingBox, captureTransformAndBounds, calculateAdjustedTransform } =
            useWorkflowBoundingBox(id);

        const { undo, redo } = undoRedoStore;
        const { undoStackLength } = storeToRefs(undoRedoStore);
        const { ctrl_z, ctrl_shift_z, meta_z, meta_shift_z } = useMagicKeys();

        const undoKeys = logicOr(ctrl_z, meta_z);
        const redoKeys = logicOr(ctrl_shift_z, meta_shift_z);

        whenever(logicAnd(undoKeys, logicNot(redoKeys)), undo);
        whenever(redoKeys, redo);

        const activityBar = ref(null);
        const workflowGraph = ref(null);
        const reportActive = computed(() => activityBar.value?.isActiveSideBar("workflow-editor-report"));

        const parameters = ref(null);

        function ensureParametersSet() {
            parameters.value = getUntypedWorkflowParameters(steps.value);
        }

        function showAttributes(args) {
            ensureParametersSet();
            stateStore.activeNodeId = null;
            activityBar.value?.setActiveSideBar("workflow-editor-attributes");
            if (args?.highlight) {
                this.highlightAttribute = args.highlight;
            }
        }

        const name = ref("Unnamed Workflow");
        const setNameActionHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (name.value = value),
            showAttributes,
            "set workflow name",
        );
        /** user set name. queues an undo/redo action */
        function setName(newName) {
            if (name.value !== newName) {
                setNameActionHandler.set(name.value, newName);
            }
        }

        const { report } = storeToRefs(stateStore);

        const license = ref(null);
        const setLicenseHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (license.value = value),
            showAttributes,
            "set license",
            "license",
        );
        /** user set license. queues an undo/redo action */
        function setLicense(newLicense) {
            if (license.value !== newLicense) {
                setLicenseHandler.set(license.value, newLicense);
            }
        }

        const creator = ref(null);
        const setCreatorHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (creator.value = value),
            showAttributes,
            "set creator",
            "creator",
        );
        /** user set creator. queues an undo/redo action */
        function setCreator(newCreator) {
            setCreatorHandler.set(creator.value, newCreator);
        }

        const doi = ref(null);
        const setDoiHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (doi.value = value),
            showAttributes,
            "set DOI",
        );
        function setDoi(newDoi) {
            setDoiHandler.set(doi.value, newDoi);
        }

        const annotation = ref(null);
        const setAnnotationHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (annotation.value = value),
            showAttributes,
            "modify short description",
            "annotation",
        );
        /** user set annotation. queues an undo/redo action */
        function setAnnotation(newAnnotation) {
            if (annotation.value !== newAnnotation) {
                setAnnotationHandler.set(annotation.value, newAnnotation);
            }
        }

        const readme = ref(null);
        const readmeActive = ref(false);
        const setReadmeHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (readme.value = value),
            (args) => {
                readmeActive.value = true;
                showAttributes(args);
            },
            "modify readme",
        );
        function setReadme(newReadme) {
            if (readme.value !== newReadme) {
                setReadmeHandler.set(readme.value, newReadme);
            }
        }
        // If we switch to the report, we want to close the readme editor
        // TODO: Maybe do this for other activities as well? E.g. inputs, tools...
        watch(
            () => reportActive.value,
            (newReportActive) => {
                if (newReportActive) {
                    readmeActive.value = false;
                }
            },
        );

        const help = ref(null);
        const setHelpHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (help.value = value),
            showAttributes,
            "modify help",
        );
        function setHelp(newHelp) {
            if (help.value !== newHelp) {
                setHelpHandler.set(help.value, newHelp);
            }
        }

        const logoUrl = ref(null);
        const setLogoUrlHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (logoUrl.value = value),
            showAttributes,
            "modify logo url",
        );
        function setLogoUrl(newLogoUrl) {
            if (logoUrl.value !== newLogoUrl) {
                setLogoUrlHandler.set(logoUrl.value, newLogoUrl);
            }
        }

        const tags = ref([]);

        const setTagsHandler = new SetValueActionHandler(
            undoRedoStore,
            (value) => (tags.value = structuredClone(value)),
            showAttributes,
            "change tags",
        );
        /** user set tags. queues an undo/redo action */
        function setTags(newTags) {
            setTagsHandler.set(tags.value, newTags);
            hasChanges.value = true;
        }

        watch(
            () => stateStore.activeNodeId,
            () => {
                scrollToTop();
            },
        );

        const rightPanelElement = ref(null);

        function scrollToTop() {
            rightPanelElement.value?.scrollTo({
                top: 0,
                behavior: "instant",
            });
        }

        const { comments } = storeToRefs(commentStore);
        const { getStepIndex, steps } = storeToRefs(stepStore);
        const { activeNodeId } = storeToRefs(stateStore);
        const activeStep = computed(() => {
            if (activeNodeId.value !== null) {
                return stepStore.getStep(activeNodeId.value);
            }
            return null;
        });

        const { hasChanges } = storeToRefs(stateStore);
        const initialLoading = ref(true);
        const hasInvalidConnections = computed(() => Object.keys(connectionStore.invalidConnections).length > 0);
        const hasDuplicateOutputs = computed(() => stepStore.duplicateLabels.size > 0);

        const hasErrors = computed(() => hasInvalidConnections.value || hasDuplicateOutputs.value);

        const errorText = computed(() => {
            const texts = [];

            if (hasInvalidConnections.value) {
                texts.push("invalid connections");
            }

            if (hasDuplicateOutputs.value) {
                texts.push("duplicate output labels");
            }

            return `Workflow has ${textify(texts, "and")}`;
        });

        stepStore.$subscribe((_mutation, _state) => {
            if (!initialLoading.value) {
                hasChanges.value = true;
            }
        });

        commentStore.$subscribe((_mutation, _state) => {
            if (!initialLoading.value) {
                hasChanges.value = true;
            }
        });

        async function resetStores() {
            hasChanges.value = false;
            connectionStore.$reset();
            stepStore.$reset();
            stateStore.$reset();
            commentStore.$reset();
            undoRedoStore.$reset();
            await nextTick();
        }

        onUnmounted(async () => {
            await resetStores();
            emit("update:confirmation", false);
        });

        const stepActions = useStepActions(stepStore, undoRedoStore, stateStore, connectionStore);

        const markdownEditor = ref(null);
        function insertMarkdown(markdown) {
            markdownEditor.value?.insertMarkdown(markdown);
        }

        const isNewTempWorkflow = computed(() => !props.workflowId);
        const lintData = useLintData(id, steps, datatypesMapper, annotation, readme, license, creator);

        const { specialWorkflowActivities, exitWorkflowActivity } = useSpecialWorkflowActivities(
            computed(() => ({
                hasInvalidConnections: hasInvalidConnections.value,
                lintData: lintData,
            })),
        );

        const getLabels = computed(() => fromSteps(steps.value));

        const saveWorkflowTitle = computed(() =>
            hasInvalidConnections.value ? `${errorText.value}, review and remove workflow errors.` : "Save Workflow",
        );

        const { confirm } = useConfirmDialog();
        const inputs = getWorkflowInputs();

        const credentialSteps = computed(() => {
            return Object.values(steps.value).filter(
                (step) => step.type === "tool" && step.config_form?.credentials?.length > 0,
            );
        });

        const unprivilegedToolStore = useUnprivilegedToolStore();
        const { canUseUnprivilegedTools } = storeToRefs(unprivilegedToolStore);
        const workflowActivities = useWorkflowActivities(
            "workflow-editor",
            isNewTempWorkflow,
            hasChanges,
            undoStackLength,
            canUseUnprivilegedTools,
        );

        const scrollToId = ref(null);

        function onHighlightRegion(bounds, moveTo = true) {
            workflowGraph.value.highlightGraphRegion(bounds, moveTo);
        }

        function onScrollTo(stepId) {
            scrollToId.value = stepId;
        }

        function onToolClick(toolId) {
            stateStore.activeNodeId = toolId;
            onScrollTo(toolId);
        }

        return {
            id,
            onToolClick,
            name,
            parameters,
            credentialSteps,
            workflowGraph,
            ensureParametersSet,
            showAttributes,
            setName,
            report,
            license,
            getLabels,
            setLicense,
            creator,
            setCreator,
            doi,
            setDoi,
            annotation,
            setAnnotation,
            readme,
            setReadme,
            readmeActive,
            help,
            setHelp,
            logoUrl,
            // make component look like an API workflow with logo_url alias for logoUrl
            logo_url: logoUrl,
            setLogoUrl,
            tags,
            setTags,
            rightPanelElement,
            scrollToTop,
            connectionStore,
            hasChanges,
            hasInvalidConnections,
            hasDuplicateOutputs,
            hasErrors,
            errorText,
            stepStore,
            steps,
            comments,
            nodeIndex: getStepIndex,
            datatypes,
            activeStep,
            activeNodeId,
            datatypesMapper,
            datatypesMapperLoading,
            stateStore,
            resetStores,
            initialLoading,
            stepActions,
            undoRedoStore,
            activityBar,
            reportActive,
            markdownEditor,
            insertMarkdown,
            specialWorkflowActivities,
            exitWorkflowActivity,
            isNewTempWorkflow,
            saveWorkflowTitle,
            confirm,
            inputs,
            workflowActivities,
            faKey,
            faWrench,
            showDropdown: false,
            lintData,
            onHighlightRegion,
            onScrollTo,
            scrollToId,
            getWorkflowBoundingBox,
            captureTransformAndBounds,
            calculateAdjustedTransform,
        };
    },
    data() {
        return {
            versions: [],
            labels: {},
            loadingWorkflow: false,
            services: null,
            stateMessages: [],
            insertedStateMessages: [],
            refactorActions: [],
            highlightAttribute: null,
            messageTitle: null,
            messageBody: null,
            messageIsError: false,
            version: this.initialVersion,
            saveAsName: null,
            saveAsAnnotation: null,
            showSaveAsModal: false,
            transform: { x: 0, y: 0, k: 1 },
            graphOffset: { left: 0, top: 0, width: 0, height: 0 },
            debounceTimer: null,
            showSaveChangesModal: false,
            navUrl: "",
            faArrowLeft,
            faArrowRight,
            faTimes,
            faCog,
            faSave,
        };
    },
    computed: {
        activeNodeType() {
            return this.activeStep?.type;
        },
        hasActiveNodeDefault() {
            return this.activeStep && this.activeStep?.type != "tool";
        },
    },
    watch: {
        id(newId, oldId) {
            if (oldId) {
                this._loadCurrent(newId, undefined, true);
            }
        },
        annotation(newAnnotation, oldAnnotation) {
            if (newAnnotation != oldAnnotation) {
                this.hasChanges = true;
            }
        },
        name(newName, oldName) {
            if (newName != oldName) {
                this.hasChanges = true;
            }
        },
        readme(newReadme, oldReadme) {
            if (newReadme != oldReadme) {
                this.hasChanges = true;
            }
        },
        help(newHelp, oldHelp) {
            if (newHelp != oldHelp) {
                this.hasChanges = true;
            }
        },
        logoUrl(newLogoUrl, oldLogoUrl) {
            if (newLogoUrl != oldLogoUrl) {
                this.hasChanges = true;
            }
        },
        hasChanges() {
            this.$emit("update:confirmation", this.hasChanges);
        },
        initialVersion(newVal, oldVal) {
            if (newVal != oldVal && oldVal === undefined) {
                this.version = this.initialVersion;
            }
        },
    },
    async created() {
        this.services = new Services();
        this.lastQueue = new LastQueue();
        await this._loadCurrent(this.id, this.version, true);
        this.initialLoading = false;
    },
    methods: {
        onUpdateStep(step) {
            this.stepStore.updateStep(step);
        },
        updateStep(id, partialStep) {
            this.stepActions.updateStep(id, partialStep);
        },
        onUpdateStepPosition(stepId, position) {
            this.stepActions.setPosition(this.steps[stepId], position);
        },
        async onAttemptRefactor(actions) {
            if (this.hasChanges) {
                const r = window.confirm(
                    "You've made changes to your workflow that need to be saved before attempting the requested action. Save those changes and continue?",
                );
                if (r == false) {
                    return;
                }

                if (await this.onSave()) {
                    this.refactorActions = actions;
                }
            } else {
                this.refactorActions = actions;
            }
        },
        // synchronize modal handling through this object so we can convert it to be
        // be reactive at some point.
        onWorkflowError(message, response) {
            this.messageTitle = message;
            this.messageBody = response.toString();
            this.messageIsError = true;
        },
        onWorkflowMessage(title, body) {
            this.messageTitle = title;
            this.messageBody = body;
            this.messageIsError = false;
        },
        hideModal() {
            this.messageTitle = null;
            this.messageBody = null;
            this.messageIsError = false;
        },
        async onRefactor(response) {
            // Store transform and bounds before resetting to adjust for coordinate shifts
            const { transform: transformBefore, bounds: boundsBefore } = this.captureTransformAndBounds(this.transform);

            await this.resetStores();
            await fromSimple(this.id, response.workflow);
            await this._loadEditorData(response.workflow);

            // Adjust for coordinate shifts so nodes appear in the same position
            // and stateStore.position is synced with the d3 transform
            this.adjustForCoordinateShift(transformBefore, boundsBefore);
        },
        onChange() {
            this.hasChanges = true;
        },
        onChangePostJobActions(nodeId, postJobActions) {
            const partialStep = { post_job_actions: postJobActions };
            this.stepActions.updateStep(nodeId, partialStep);
        },
        onRemove(nodeId) {
            this.stepActions.removeStep(this.steps[nodeId]);
        },
        onEditSubworkflow(contentId) {
            const editUrl = `/workflows/edit?workflow_id=${contentId}`;
            this.onNavigate(editUrl);
        },
        async onClone(stepId) {
            const sourceStep = this.steps[parseInt(stepId)];
            this.stepActions.copyStep({
                ...sourceStep,
                id: null,
                uuid: null,
                position: defaultPosition(this.graphOffset, this.transform),
            });
        },
        onInsertTool(tool_id, tool_name, toolUuid) {
            this._insertStep(tool_id, tool_name, "tool", undefined, toolUuid);
        },
        async onInsertModule(module_id, module_name, state) {
            this._insertStep(module_name, module_name, module_id, state);
        },
        onInsertWorkflow(workflow_id, workflow_name) {
            this._insertStep(workflow_id, workflow_name, "subworkflow");
        },
        copyIntoWorkflow(id) {
            // Load workflow definition
            this.onWorkflowMessage("Importing workflow", "progress");
            getWorkflowFull(id).then((data) => {
                const action = new CopyIntoWorkflowAction(
                    this.id,
                    data,
                    defaultPosition(this.graphOffset, this.transform),
                    true,
                );
                this.undoRedoStore.applyAction(action);
                // Determine if any parameters were 'upgraded' and provide message
                const insertedStateMessages = getStateUpgradeMessages(data);
                this.onInsertedStateMessages(insertedStateMessages);
            });
        },
        async onInsertWorkflowSteps(workflowId, stepCount) {
            if (stepCount < 10) {
                this.copyIntoWorkflow(workflowId);
            } else {
                const confirmed = await ConfirmDialog.confirm(
                    `Warning this will add ${stepCount} new steps into your current workflow.  You may want to consider using a subworkflow instead.`,
                );
                if (confirmed) {
                    this.copyIntoWorkflow(workflowId);
                }
            }
        },
        onDownload() {
            window.location = `${getAppRoot()}api/workflows/${this.id}/download?format=json-download`;
        },
        async doSaveAs() {
            if (!this.saveAsName && !this.nameValidate()) {
                return;
            }
            const rename_name = this.saveAsName ?? `SavedAs_${this.name}`;
            const rename_annotation = this.saveAsAnnotation ?? "";

            try {
                const newSaveAsWf = { ...this, name: rename_name, annotation: rename_annotation };
                const { id, name, number_of_steps } = await this.services.createWorkflow(newSaveAsWf);
                const message = `Created new workflow '${name}' with ${number_of_steps} steps.`;
                this.hasChanges = false;
                await this.routeToWorkflow(id);
                Toast.success(message);
            } catch (e) {
                const errorHeading = `Saving workflow as '${rename_name}' failed`;
                this.onWorkflowError(errorHeading, errorMessageAsString(e) || "Please contact an administrator.", {
                    Ok: () => {
                        this.hideModal();
                    },
                });
            }
        },
        onSaveAs() {
            this.showSaveAsModal = true;
        },
        async createNewWorkflow() {
            await this.saveOrCreate();
            this.$router.push("/workflows/edit");
        },
        async saveOrCreate() {
            if (this.hasErrors) {
                const confirmed = await this.confirm(
                    `${this.errorText}. You can save the workflow, but it may not run correctly.`,
                    {
                        id: "save-workflow-confirmation",
                        okTitle: "Save Workflow",
                    },
                );

                if (!confirmed) {
                    return;
                }
            }

            if (this.isNewTempWorkflow) {
                await this.onCreate();
            } else {
                await this.onSave();
            }
        },
        async onActivityClicked(activityId) {
            if (activityId === "save-and-exit") {
                await this.saveOrCreate();
                this.$router.push("/workflows/list");
            }

            if (activityId === "exit") {
                this.$router.push("/workflows/list");
            }

            if (activityId === "workflow-download") {
                this.onDownload();
            }

            if (activityId === "workflow-upgrade") {
                this.onUpgrade();
            }

            if (activityId === "workflow-run") {
                this.onRun();
            }

            if (activityId === "save-workflow") {
                await this.saveOrCreate();
            }

            if (activityId === "save-workflow-as") {
                this.onSaveAs();
            }

            if (activityId === "workflow-create") {
                this.createNewWorkflow();
            }
        },
        onAnnotation(nodeId, newAnnotation) {
            this.stepActions.setAnnotation(this.steps[nodeId], newAnnotation);
        },
        async routeToWorkflow(id) {
            // map scoped stores to existing stores, before updating the id
            const { addScopePointer } = useScopePointerStore();
            addScopePointer(id, this.id);

            this.id = id;
            if (await this.onSave()) {
                this.hasChanges = false;
                this.$router.replace({ query: { id } });
            }
        },
        async onCreate() {
            if (!this.nameValidate()) {
                return;
            }
            try {
                const { id, name, number_of_steps } = await this.services.createWorkflow(this);
                const message = `Created new workflow '${name}' with ${number_of_steps} steps.`;
                this.hasChanges = false;
                this.$emit("skipNextReload");
                await this.routeToWorkflow(id);
                Toast.success(message);
            } catch (e) {
                this.onWorkflowError(
                    "Creating workflow failed",
                    errorMessageAsString(e) || "Please contact an administrator.",
                    {
                        Ok: () => {
                            this.hideModal();
                        },
                    },
                );
            }
        },
        nameValidate() {
            if (!this.name) {
                Toast.error("Please provide a name for your workflow.");
                this.showAttributes();
                return false;
            }
            return true;
        },
        onSetData(stepId, newData) {
            this.lastQueue
                .enqueue(() => getModule(newData, stepId, this.stateStore.setLoadingState))
                .then((data) => {
                    const partialStep = {
                        content_id: data.content_id,
                        inputs: data.inputs,
                        outputs: data.outputs,
                        config_form: data.config_form,
                        tool_state: data.tool_state,
                        tool_version: data.tool_version,
                        errors: data.errors,
                    };
                    this.stepActions.updateStep(stepId, partialStep);
                });
        },
        onLabel(nodeId, newLabel) {
            this.stepActions.setLabel(this.steps[nodeId], newLabel);
        },
        onUpgrade() {
            this.onAttemptRefactor([{ action_type: "upgrade_all_steps" }]);
        },
        onReportUpdate(markdown) {
            this.hasChanges = true;
            this.report.markdown = markdown;
        },
        onRun() {
            this.onNavigate(`/workflows/run?id=${this.id}`, false, false, true);
        },
        async onNavigate(url, forceSave = false, ignoreChanges = false, appendVersion = false) {
            let proceed = false;
            if (this.isNewTempWorkflow) {
                await this.onCreate();
                proceed = true;
            } else if (this.hasChanges && !forceSave && !ignoreChanges) {
                // if there are changes, prompt user to save or discard or cancel
                this.navUrl = url;
                this.showSaveChangesModal = true;
            } else if (forceSave) {
                // when forceSave is true, save the workflow before navigating
                proceed = await this.onSave();
            } else {
                // no changes to save, proceed with navigation
                proceed = true;
            }
            if (!proceed) {
                return;
            }

            if (appendVersion && this.version !== undefined) {
                url += `&version=${this.version}`;
            }
            this.hasChanges = false;
            await nextTick();
            this.$router.push(url);
        },
        /** Saves the workflow, and loads it onto the editor by calling `_loadCurrent`.
         * @returns true if save was successful, false otherwise
         */
        async onSave() {
            if (!this.nameValidate()) {
                return false;
            }
            const lastActiveNodeId = this.activeNodeId;

            try {
                this.loadingWorkflow = true;

                const data = await saveWorkflow(this);

                const versions = await getVersions(this.id);
                this.versions = versions;

                // If version is not defined, set it to the latest version
                if (this.version === undefined || this.version === null) {
                    this.version = versions[versions.length - 1].version;
                }

                await this._loadCurrent(this.id, data.version);
            } catch (response) {
                this.onWorkflowError("Saving workflow failed...", response, {
                    Ok: () => {
                        this.hideModal();
                    },
                });
                return false;
            } finally {
                this.stateStore.activeNodeId = lastActiveNodeId;
                this.loadingWorkflow = false;
            }
            return true;
        },
        onVersion(version) {
            if (version != this.version) {
                if (this.hasChanges) {
                    const r = window.confirm(
                        "There are unsaved changes to your workflow which will be lost. Continue ?",
                    );
                    if (r == false) {
                        return;
                    }
                }
                this.version = version;
                this._loadCurrent(this.id, version);
            }
        },
        async _insertStep(contentId, name, type, state, toolUuid) {
            const action = new InsertStepAction(this.stepStore, this.stateStore, {
                contentId,
                name,
                type,
                position: defaultPosition(this.graphOffset, this.transform),
            });

            this.undoRedoStore.applyAction(action);
            const stepData = action.getNewStepData();

            const response = await getModule(
                { name, type, content_id: contentId, tool_state: state, tool_uuid: toolUuid },
                stepData.id,
                this.stateStore.setLoadingState,
            );

            const updatedStep = {
                ...stepData,
                tool_uuid: toolUuid,
                tool_state: response.tool_state,
                inputs: response.inputs,
                outputs: response.outputs,
                config_form: response.config_form,
            };

            this.stepStore.updateStep(updatedStep);
            action.updateStepData = updatedStep;

            this.stateStore.activeNodeId = stepData.id;
        },
        async _loadEditorData(data) {
            if (data.name !== undefined) {
                this.name = data.name;
            }
            if (data.annotation !== undefined) {
                this.annotation = data.annotation;
            }
            if (data.readme !== undefined) {
                this.readme = data.readme;
            }
            if (data.help !== undefined) {
                this.help = data.help;
            }
            if (data.logo_url !== undefined) {
                this.logoUrl = data.logo_url;
            }
            if (data.version !== undefined) {
                this.version = data.version;
            }

            const report = data.report || {};
            const markdown = report.markdown || reportDefault;
            this.report.markdown = markdown;
            this.hideModal();
            this.stateMessages = getStateUpgradeMessages(data);
            const has_changes = this.stateMessages.length > 0;
            this.tags = data.tags;
            this.license = data.license;
            this.creator = data.creator;
            this.doi = data.doi;
            getVersions(this.id).then((versions) => {
                this.versions = versions;
            });
            await Vue.nextTick();
            this.hasChanges = has_changes;
        },
        /**
         * Fetches and loads the workflow data for the given id and version into the editor.
         * @param {string} id - The workflow ID
         * @param {number|undefined} version - The workflow version number
         * @param {boolean|undefined} fitGraph - Whether to reset the workflow transform and positioning to the default fit
         */
        async _loadCurrent(id, version = undefined, fitGraph = false) {
            if (!this.isNewTempWorkflow) {
                this.loadingWorkflow = true;

                // Store the current transform and bounding box before loading; to adjust for coordinate shifts
                const { transform: transformBefore, bounds: boundsBefore } = this.captureTransformAndBounds(
                    this.transform,
                );
                try {
                    // Load editor view workflow data
                    const data = await this.lastQueue.enqueue(() => getWorkflowFull(id, version));

                    // Reset stores and load new data onto editor
                    await this.resetStores();
                    await fromSimple(id, data);
                    await this._loadEditorData(data);
                } catch (e) {
                    this.onWorkflowError("Loading workflow failed...", e);
                }

                await until(() => this.datatypesMapperLoading).toBe(false);
                await nextTick();

                if (fitGraph) {
                    this.workflowGraph.fitWorkflow();
                } else {
                    // If we are not fitting the graph, adjust for coordinate shifts so the nodes appear in the same position
                    this.adjustForCoordinateShift(transformBefore, boundsBefore);
                }
                this.loadingWorkflow = false;
            }
        },
        /**
         * Adjusts the workflow graph transform to account for coordinate shifts that may be
         * computed by the backend when a workflow step order/positioning changes.
         * @param transformBefore The transform we had before refetching the workflow
         * @param boundsBefore The bounding box min coordinates we had before refetching the workflow
         */
        adjustForCoordinateShift(transformBefore, boundsBefore) {
            const adjustedTransform = this.calculateAdjustedTransform(transformBefore, boundsBefore);

            // TODO: Once we migrate to Composition API we can probably handle this within the workflowBoundingBox
            // and d3Zoom composables
            this.workflowGraph.setTransform(adjustedTransform);

            // TODO: Verify if setting scale is still needed after setting full transform
            //       I still needed to set scale separately otherwise it would reset to 1
            this.stateStore.scale = adjustedTransform.k;
        },
        onLicense(license) {
            if (this.license != license) {
                this.hasChanges = true;
                this.setLicense(license);
            }
        },
        onCreator(creator) {
            if (this.creator != creator) {
                this.hasChanges = true;
                this.setCreator(creator);
            }
        },
        onDoi(doi) {
            if (this.doi != doi) {
                this.hasChanges = true;
                this.setDoi(doi);
            }
        },
        onInsertedStateMessages(insertedStateMessages) {
            this.insertedStateMessages = insertedStateMessages;
            this.hideModal();
        },
        resetMessage() {
            this.messageTitle = null;
            this.messageBody = null;
            this.messageError = false;
        },
    },
};
</script>

<style scoped lang="scss">
@import "@/style/scss/theme/blue.scss";

.editor-top-bar {
    background: $brand-light;
    color: $brand-dark;
    font-size: 1rem;
    font-weight: 700;
    display: flex;
    justify-content: space-between;
    height: 2.5rem;
    padding: 0 1rem;

    & > span {
        overflow: hidden;
        display: flex;
        align-items: center;
    }

    .editor-title {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
}

.reset-wheel {
    position: absolute;
    left: 1rem;
    bottom: 1rem;
    cursor: pointer;
    z-index: 2000;
}

.workflow-center {
    z-index: 0;
    display: flex;
    flex-direction: column;
    flex-grow: 1;
    overflow-x: auto;
    overflow-y: hidden;
    width: 100%;
}
</style>
