import {
  ReduxAction,
  ReduxActionErrorTypes,
  ReduxActionTypes,
} from "constants/ReduxActionConstants";
import {
  MultipleWidgetDeletePayload,
  updateAndSaveLayout,
  WidgetAddChild,
  WidgetAddChildren,
  WidgetDelete,
  WidgetMove,
  WidgetResize,
} from "actions/pageActions";
import {
  CanvasWidgetsReduxState,
  FlattenedWidgetProps,
} from "reducers/entityReducers/canvasWidgetsReducer";
import {
  getFocusedWidget,
  getSelectedWidget,
  getWidget,
  getWidgets,
} from "./selectors";
import {
  generateWidgetProps,
  updateWidgetPosition,
} from "utils/WidgetPropsUtils";
import {
  all,
  call,
  fork,
  put,
  select,
  takeEvery,
  takeLatest,
} from "redux-saga/effects";
import { convertToString, getNextEntityName } from "utils/AppsmithUtils";
import {
  batchUpdateWidgetProperty,
  DeleteWidgetPropertyPayload,
  SetWidgetDynamicPropertyPayload,
  UpdateWidgetPropertyPayload,
  UpdateWidgetPropertyRequestPayload,
} from "actions/controlActions";
import {
  DynamicPath,
  getEntityDynamicBindingPathList,
  getWidgetDynamicPropertyPathList,
  getWidgetDynamicTriggerPathList,
  isChildPropertyPath,
  isDynamicValue,
  isPathADynamicBinding,
  isPathADynamicTrigger,
} from "utils/DynamicBindingUtils";
import { WidgetProps } from "widgets/BaseWidget";
import _, { cloneDeep, flattenDeep, isString, set, remove } from "lodash";
import WidgetFactory from "utils/WidgetFactory";
import {
  buildWidgetBlueprint,
  executeWidgetBlueprintOperations,
  traverseTreeAndExecuteBlueprintChildOperations,
} from "sagas/WidgetBlueprintSagas";
import { resetWidgetMetaProperty } from "actions/metaActions";
import {
  GridDefaults,
  MAIN_CONTAINER_WIDGET_ID,
  RenderModes,
  WIDGET_DELETE_UNDO_TIMEOUT,
  WidgetType,
  WidgetTypes,
} from "constants/WidgetConstants";
import WidgetConfigResponse, {
  GRID_DENSITY_MIGRATION_V1,
} from "mockResponses/WidgetConfigResponse";
import {
  flushDeletedWidgets,
  getCopiedWidgets,
  getDeletedWidgets,
  saveCopiedWidgets,
  saveDeletedWidgets,
} from "utils/storage";
import { generateReactKey } from "utils/generators";
import { flashElementById } from "utils/helpers";
import AnalyticsUtil from "utils/AnalyticsUtil";
import log from "loglevel";
import { navigateToCanvas } from "pages/Editor/Explorer/Widgets/utils";
import {
  getCurrentApplicationId,
  getCurrentPageId,
} from "selectors/editorSelectors";
import {
  closePropertyPane,
  closeTableFilterPane,
  forceOpenPropertyPane,
} from "actions/widgetActions";
import {
  selectMultipleWidgetsInitAction,
  selectWidgetInitAction,
} from "actions/widgetSelectionActions";

import { getDataTree } from "selectors/dataTreeSelectors";
import {
  clearEvalPropertyCacheOfWidget,
  validateProperty,
} from "./EvaluationsSaga";
import { WidgetBlueprint } from "reducers/entityReducers/widgetConfigReducer";
import { Toaster } from "components/ads/Toast";
import { Variant } from "components/ads/common";
import { ColumnProperties } from "components/designSystems/appsmith/TableComponent/Constants";
import {
  getAllPathsFromPropertyConfig,
  nextAvailableRowInContainer,
} from "entities/Widget/utils";
import { getAllPaths } from "workers/evaluationUtils";
import {
  createMessage,
  ERROR_ADD_WIDGET_FROM_QUERY,
  ERROR_WIDGET_COPY_NO_WIDGET_SELECTED,
  ERROR_WIDGET_CUT_NO_WIDGET_SELECTED,
  WIDGET_COPY,
  WIDGET_CUT,
  WIDGET_DELETE,
  WIDGET_BULK_DELETE,
  ERROR_WIDGET_COPY_NOT_ALLOWED,
} from "constants/messages";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import LOG_TYPE from "entities/AppsmithConsole/logtype";
import {
  checkIfPastingIntoListWidget,
  doesTriggerPathsContainPropertyPath,
  getParentWidgetIdForPasting,
  getWidgetChildren,
  handleSpecificCasesWhilePasting,
} from "./WidgetOperationUtils";
import { getSelectedWidgets } from "selectors/ui";
import { getParentWithEnhancementFn } from "./WidgetEnhancementHelpers";
import { widgetSelectionSagas } from "./WidgetSelectionSagas";
function* getChildWidgetProps(
  parent: FlattenedWidgetProps,
  params: WidgetAddChild,
  widgets: { [widgetId: string]: FlattenedWidgetProps },
) {
  const { leftColumn, newWidgetId, props, topRow, type } = params;
  let { columns, parentColumnSpace, parentRowSpace, rows, widgetName } = params;
  let minHeight = undefined;
  /* eslint-disable @typescript-eslint/no-unused-vars */
  const { blueprint = undefined, ...restDefaultConfig } = {
    ...(WidgetConfigResponse as any).config[type],
  };
  if (!widgetName) {
    const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName);
    const entityNames = yield call(getEntityNames);

    widgetName = getNextEntityName(restDefaultConfig.widgetName, [
      ...widgetNames,
      ...entityNames,
    ]);
  }
  if (type === WidgetTypes.CANVAS_WIDGET) {
    columns =
      (parent.rightColumn - parent.leftColumn) * parent.parentColumnSpace;
    parentColumnSpace = 1;
    rows = (parent.bottomRow - parent.topRow) * parent.parentRowSpace;
    parentRowSpace = 1;
    minHeight = rows;
    if (props) props.children = [];
  }

  const widgetProps = {
    ...restDefaultConfig,
    ...props,
    columns,
    rows,
    minHeight,
  };
  const widget = generateWidgetProps(
    parent,
    type,
    leftColumn,
    topRow,
    parentRowSpace,
    parentColumnSpace,
    widgetName,
    widgetProps,
    restDefaultConfig.version,
  );

  widget.widgetId = newWidgetId;
  return widget;
}
type GeneratedWidgetPayload = {
  widgetId: string;
  widgets: { [widgetId: string]: FlattenedWidgetProps };
};
function* generateChildWidgets(
  parent: FlattenedWidgetProps,
  params: WidgetAddChild,
  widgets: { [widgetId: string]: FlattenedWidgetProps },
  propsBlueprint?: WidgetBlueprint,
): any {
  // Get the props for the widget
  const widget = yield getChildWidgetProps(parent, params, widgets);

  // Add the widget to the canvasWidgets
  // We need this in here as widgets will be used to get the current widget
  widgets[widget.widgetId] = widget;

  // Get the default config for the widget from WidgetConfigResponse
  const defaultConfig = {
    ...(WidgetConfigResponse as any).config[widget.type],
  };

  // If blueprint is provided in the params, use that
  // else use the blueprint available in WidgetConfigResponse
  // else there is no blueprint for this widget
  const blueprint =
    propsBlueprint || { ...defaultConfig.blueprint } || undefined;

  // If there is a blueprint.view
  // We need to generate the children based on the view
  if (blueprint && blueprint.view) {
    // Get the list of children props in WidgetAddChild format
    const childWidgetList: WidgetAddChild[] = yield call(
      buildWidgetBlueprint,
      blueprint,
      widget.widgetId,
    );
    // For each child props
    const childPropsList: GeneratedWidgetPayload[] = yield all(
      childWidgetList.map((props: WidgetAddChild) => {
        // Generate full widget props
        // Notice that we're passing the blueprint if it exists.
        return generateChildWidgets(
          widget,
          props,
          widgets,
          props.props?.blueprint,
        );
      }),
    );
    // Start children array from scratch
    widget.children = [];
    childPropsList.forEach((props: GeneratedWidgetPayload) => {
      // Push the widgetIds of the children generated above into the widget.children array
      widget.children.push(props.widgetId);
      // Add the list of widgets generated into the canvasWidgets
      widgets = props.widgets;
    });
  }

  // Finally, add the widget to the canvasWidgets
  // This is different from above, as this is the final widget props with
  // a fully populated widget.children property
  widgets[widget.widgetId] = widget;

  // Some widgets need to run a few operations like modifying props or adding an action
  // these operations can be performed on the parent of the widget we're adding
  // therefore, we pass all widgets to executeWidgetBlueprintOperations
  // blueprint.operations contain the set of operations to perform to update the canvasWidgets
  if (blueprint && blueprint.operations && blueprint.operations.length > 0) {
    // Finalize the canvasWidgets with everything that needs to be updated
    widgets = yield call(
      executeWidgetBlueprintOperations,
      blueprint.operations,
      widgets,
      widget.widgetId,
    );
  }

  // Add the parentId prop to this widget
  widget.parentId = parent.widgetId;
  // Remove the blueprint from the widget (if any)
  // as blueprints are not useful beyond this point.
  delete widget.blueprint;

  // deleting propertyPaneEnchancements too as it shouldn't go in dsl because
  // function can't be cloned into dsl

  // instead of passing whole enhancments function in widget props, we are just setting
  // enhancments as true so that we know this widget contains enhancments
  if ("enhancements" in widget) {
    widget.enhancements = true;
  }

  return { widgetId: widget.widgetId, widgets };
}

/**
 * this saga is called when we drop a widget on the canvas.
 *
 * @param addChildAction
 */
export function* addChildSaga(addChildAction: ReduxAction<WidgetAddChild>) {
  try {
    const start = performance.now();
    Toaster.clear();

    // NOTE: widgetId here is the parentId of the dropped widget ( we should rename it to avoid confusion )
    const { widgetId } = addChildAction.payload;
    // Get the current parent widget whose child will be the new widget.
    const stateParent: FlattenedWidgetProps = yield select(getWidget, widgetId);
    // const parent = Object.assign({}, stateParent);
    // Get all the widgets from the canvasWidgetsReducer
    const stateWidgets = yield select(getWidgets);
    let widgets = Object.assign({}, stateWidgets);
    // Generate the full WidgetProps of the widget to be added.
    const childWidgetPayload: GeneratedWidgetPayload = yield generateChildWidgets(
      stateParent,
      addChildAction.payload,
      widgets,
    );
    // Update widgets to put back in the canvasWidgetsReducer
    // TODO(abhinav): This won't work if dont already have an empty children: []
    const parent = {
      ...stateParent,
      children: [...(stateParent.children || []), childWidgetPayload.widgetId],
    };

    widgets[parent.widgetId] = parent;
    AppsmithConsole.info({
      text: "Widget was created",
      source: {
        type: ENTITY_TYPE.WIDGET,
        id: childWidgetPayload.widgetId,
        name:
          childWidgetPayload.widgets[childWidgetPayload.widgetId].widgetName,
      },
    });
    log.debug("add child computations took", performance.now() - start, "ms");

    // some widgets need to update property of parent if the parent have CHILD_OPERATIONS
    // so here we are traversing up the tree till we get to MAIN_CONTAINER_WIDGET_ID
    // while travesring, if we find any widget which has CHILD_OPERATION, we will call the fn in it
    const updatedWidgets: {
      [widgetId: string]: FlattenedWidgetProps;
    } = yield call(
      traverseTreeAndExecuteBlueprintChildOperations,
      parent,
      addChildAction.payload.newWidgetId,
      widgets,
    );

    widgets = updatedWidgets;

    yield put({
      type: ReduxActionTypes.WIDGET_CHILD_ADDED,
      payload: {
        widgetId: childWidgetPayload.widgetId,
        type: addChildAction.payload.type,
      },
    });
    yield put(updateAndSaveLayout(widgets));

    // go up till MAIN_CONTAINER, if there is a operation CHILD_OPERATIONS IN ANY PARENT,
    // call execute
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
      payload: {
        action: ReduxActionTypes.WIDGET_ADD_CHILD,
        error,
      },
    });
  }
}

// This is different from addChildSaga
// It does not go through the blueprint based creation
// It simply uses the provided widget props to create widgets
// Use this only when we're 100% sure of all the props the children will need
export function* addChildrenSaga(
  addChildrenAction: ReduxAction<WidgetAddChildren>,
) {
  try {
    const { children, widgetId } = addChildrenAction.payload;
    const stateWidgets = yield select(getWidgets);
    const widgets = { ...stateWidgets };
    const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName);
    const entityNames = yield call(getEntityNames);

    children.forEach((child) => {
      // Create only if it doesn't already exist
      if (!widgets[child.widgetId]) {
        const defaultConfig: any = WidgetConfigResponse.config[child.type];
        const newWidgetName = getNextEntityName(defaultConfig.widgetName, [
          ...widgetNames,
          ...entityNames,
        ]);
        // update the list of widget names for the next iteration
        widgetNames.push(newWidgetName);
        widgets[child.widgetId] = {
          ...child,
          widgetName: newWidgetName,
          renderMode: RenderModes.CANVAS,
        };

        const existingChildren = widgets[widgetId].children || [];

        widgets[widgetId] = {
          ...widgets[widgetId],
          children: [...existingChildren, child.widgetId],
        };
      }
    });

    yield put(updateAndSaveLayout(widgets));
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
      payload: {
        action: ReduxActionTypes.WIDGET_ADD_CHILDREN,
        error,
      },
    });
  }
}

const getAllWidgetsInTree = (
  widgetId: string,
  canvasWidgets: CanvasWidgetsReduxState,
) => {
  const widget = canvasWidgets[widgetId];
  const widgetList = [widget];
  if (widget && widget.children) {
    widget.children
      .filter(Boolean)
      .forEach((childWidgetId: string) =>
        widgetList.push(...getAllWidgetsInTree(childWidgetId, canvasWidgets)),
      );
  }
  return widgetList;
};

/**
 * Note: Mutates finalWidgets[parentId].bottomRow for CANVAS_WIDGET
 * @param finalWidgets
 * @param parentId
 */
const resizeCanvasToLowestWidget = (
  finalWidgets: CanvasWidgetsReduxState,
  parentId: string,
) => {
  if (
    !finalWidgets[parentId] ||
    finalWidgets[parentId].type !== WidgetTypes.CANVAS_WIDGET
  ) {
    return;
  }

  let lowestBottomRow = Math.ceil(
    (finalWidgets[parentId].minHeight || 0) /
      GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
  );
  const childIds = finalWidgets[parentId].children || [];
  // find lowest row
  childIds.forEach((cId) => {
    const child = finalWidgets[cId];
    if (child.bottomRow > lowestBottomRow) {
      lowestBottomRow = child.bottomRow;
    }
  });
  finalWidgets[parentId].bottomRow =
    (lowestBottomRow + GridDefaults.CANVAS_EXTENSION_OFFSET) *
    GridDefaults.DEFAULT_GRID_ROW_HEIGHT;
};

export function* deleteAllSelectedWidgetsSaga(
  deleteAction: ReduxAction<MultipleWidgetDeletePayload>,
) {
  try {
    const { disallowUndo = false, isShortcut } = deleteAction.payload;
    const stateWidgets = yield select(getWidgets);
    const widgets = { ...stateWidgets };
    const selectedWidgets: string[] = yield select(getSelectedWidgets);
    if (!(selectedWidgets && selectedWidgets.length !== 1)) return;
    const widgetsToBeDeleted = yield all(
      selectedWidgets.map((eachId) => {
        return call(getAllWidgetsInTree, eachId, widgets);
      }),
    );
    const falttendedWidgets: any = flattenDeep(widgetsToBeDeleted);
    const parentUpdatedWidgets = falttendedWidgets.reduce(
      (allWidgets: any, eachWidget: any) => {
        const { parentId, widgetId } = eachWidget;
        const stateParent: FlattenedWidgetProps = allWidgets[parentId];
        let parent = { ...stateParent };
        if (parent.children) {
          parent = {
            ...parent,
            children: parent.children.filter((c) => c !== widgetId),
          };
          allWidgets[parentId] = parent;
        }
        return allWidgets;
      },
      widgets,
    );
    const finalWidgets: CanvasWidgetsReduxState = _.omit(
      parentUpdatedWidgets,
      falttendedWidgets.map((widgets: any) => widgets.widgetId),
    );
    // assuming only widgets with same parent can be selected
    const parentId = widgets[selectedWidgets[0]].parentId;
    resizeCanvasToLowestWidget(finalWidgets, parentId);

    yield put(updateAndSaveLayout(finalWidgets));
    yield put(selectWidgetInitAction(""));
    const bulkDeleteKey = selectedWidgets.join(",");
    const saveStatus: boolean = yield saveDeletedWidgets(
      falttendedWidgets,
      bulkDeleteKey,
    );
    if (saveStatus && !disallowUndo) {
      // close property pane after delete
      yield put(closePropertyPane());
      yield put(closeTableFilterPane());
      Toaster.show({
        text: createMessage(WIDGET_BULK_DELETE, `${selectedWidgets.length}`),
        hideProgressBar: false,
        variant: Variant.success,
        dispatchableAction: {
          type: ReduxActionTypes.UNDO_DELETE_WIDGET,
          payload: {
            widgetId: bulkDeleteKey,
          },
        },
      });
      setTimeout(() => {
        if (bulkDeleteKey) {
          flushDeletedWidgets(bulkDeleteKey);
          falttendedWidgets.map((widget: any) => {
            AppsmithConsole.info({
              logType: LOG_TYPE.ENTITY_DELETED,
              text: "Widget was deleted",
              source: {
                name: widget.widgetName,
                type: ENTITY_TYPE.WIDGET,
                id: widget.widgetId,
              },
            });
          });
        }
      }, WIDGET_DELETE_UNDO_TIMEOUT);
    }
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
      payload: {
        action: ReduxActionTypes.WIDGET_DELETE,
        error,
      },
    });
  }
}

export function* deleteSagaInit(deleteAction: ReduxAction<WidgetDelete>) {
  const { widgetId } = deleteAction.payload;
  const selectedWidget = yield select(getSelectedWidget);
  const selectedWidgets: string[] = yield select(getSelectedWidgets);
  if (selectedWidgets.length > 1) {
    yield put({
      type: ReduxActionTypes.WIDGET_BULK_DELETE,
      payload: deleteAction.payload,
    });
  }
  if (!!widgetId || !!selectedWidget) {
    yield put({
      type: ReduxActionTypes.WIDGET_SINGLE_DELETE,
      payload: deleteAction.payload,
    });
  }
}

export function* deleteSaga(deleteAction: ReduxAction<WidgetDelete>) {
  try {
    let { parentId, widgetId } = deleteAction.payload;
    const { disallowUndo, isShortcut } = deleteAction.payload;

    if (!widgetId) {
      const selectedWidget: FlattenedWidgetProps | undefined = yield select(
        getSelectedWidget,
      );
      if (!selectedWidget) return;

      // if widget is not deletable, don't don anything
      if (selectedWidget.isDeletable === false) return false;

      widgetId = selectedWidget.widgetId;
      parentId = selectedWidget.parentId;
    }

    if (widgetId && parentId) {
      const stateWidgets = yield select(getWidgets);
      const widgets = { ...stateWidgets };
      const stateWidget: WidgetProps = yield select(getWidget, widgetId);
      const widget = { ...stateWidget };

      const stateParent: FlattenedWidgetProps = yield select(
        getWidget,
        parentId,
      );
      let parent = { ...stateParent };

      const analyticsEvent = isShortcut
        ? "WIDGET_DELETE_VIA_SHORTCUT"
        : "WIDGET_DELETE";

      AnalyticsUtil.logEvent(analyticsEvent, {
        widgetName: widget.widgetName,
        widgetType: widget.type,
      });

      // Remove entry from parent's children

      if (parent.children) {
        parent = {
          ...parent,
          children: parent.children.filter((c) => c !== widgetId),
        };
      }

      widgets[parentId] = parent;

      const otherWidgetsToDelete = getAllWidgetsInTree(widgetId, widgets);
      const saveStatus: boolean = yield saveDeletedWidgets(
        otherWidgetsToDelete,
        widgetId,
      );
      let widgetName = widget.widgetName;
      // SPECIAL HANDLING FOR TABS IN A TABS WIDGET
      if (parent.type === WidgetTypes.TABS_WIDGET && widget.tabName) {
        widgetName = widget.tabName;
      }
      if (saveStatus && !disallowUndo) {
        // close property pane after delete
        yield put(closePropertyPane());
        Toaster.show({
          text: createMessage(WIDGET_DELETE, widgetName),
          hideProgressBar: false,
          variant: Variant.success,
          dispatchableAction: {
            type: ReduxActionTypes.UNDO_DELETE_WIDGET,
            payload: {
              widgetId,
            },
          },
        });

        setTimeout(() => {
          if (widgetId) {
            flushDeletedWidgets(widgetId);
            otherWidgetsToDelete.map((widget) => {
              AppsmithConsole.info({
                logType: LOG_TYPE.ENTITY_DELETED,
                text: "Widget was deleted",
                source: {
                  name: widget.widgetName,
                  type: ENTITY_TYPE.WIDGET,
                  id: widget.widgetId,
                },
              });
            });
          }
        }, WIDGET_DELETE_UNDO_TIMEOUT);
      }

      yield call(clearEvalPropertyCacheOfWidget, widgetName);

      let finalWidgets: CanvasWidgetsReduxState = yield call(
        updateListWidgetPropertiesOnChildDelete,
        widgets,
        widgetId,
        widgetName,
      );

      finalWidgets = _.omit(
        finalWidgets,
        otherWidgetsToDelete.map((widgets) => widgets.widgetId),
      );

      // Note: mutates finalWidgets
      resizeCanvasToLowestWidget(finalWidgets, parentId);

      yield put(updateAndSaveLayout(finalWidgets));
    }
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
      payload: {
        action: ReduxActionTypes.WIDGET_DELETE,
        error,
      },
    });
  }
}

/**
 * this saga clears out the enhancementMap, template and dynamicBindingPathList when a child
 * is deleted in list widget
 *
 * @param widgets
 * @param widgetId
 * @param widgetName
 * @param parentId
 */
export function* updateListWidgetPropertiesOnChildDelete(
  widgets: CanvasWidgetsReduxState,
  widgetId: string,
  widgetName: string,
) {
  const clone = JSON.parse(JSON.stringify(widgets));

  const parentWithEnhancementFn = getParentWithEnhancementFn(widgetId, clone);

  if (parentWithEnhancementFn?.type === "LIST_WIDGET") {
    const listWidget = parentWithEnhancementFn;

    // delete widget in template of list
    if (listWidget && widgetName in listWidget.template) {
      listWidget.template[widgetName] = undefined;
    }

    // delete dynamic binding path if any
    remove(listWidget?.dynamicBindingPathList || [], (path: any) =>
      path.key.startsWith(`template.${widgetName}`),
    );

    return clone;
  }

  return clone;
}

export function* undoDeleteSaga(action: ReduxAction<{ widgetId: string }>) {
  // Get the list of widget and its children which were deleted
  const deletedWidgets: FlattenedWidgetProps[] = yield getDeletedWidgets(
    action.payload.widgetId,
  );
  const deletedWidgetIds = action.payload.widgetId.split(",");
  if (deletedWidgets && Array.isArray(deletedWidgets)) {
    // Get the current list of widgets from reducer
    const formTree = deletedWidgets.reduce((widgetTree, each) => {
      widgetTree[each.widgetId] = each;
      return widgetTree;
    }, {} as CanvasWidgetsReduxState);
    const stateWidgets = yield select(getWidgets);
    const deletedWidgetGroups = deletedWidgetIds.map((each) => ({
      widget: formTree[each],
      widgetsToRestore: getAllWidgetsInTree(each, formTree),
    }));
    const finalWidgets = deletedWidgetGroups.reduce(
      (reducedWidgets, deletedWidgetGroup) => {
        const {
          widget: deletedWidget,
          widgetsToRestore: deletedWidgets,
        } = deletedWidgetGroup;
        let widgets = cloneDeep(reducedWidgets);

        // If the deleted widget is in fact available.
        if (deletedWidget) {
          // Log an undo event
          AnalyticsUtil.logEvent("WIDGET_DELETE_UNDO", {
            widgetName: deletedWidget.widgetName,
            widgetType: deletedWidget.type,
          });
        }

        // For each deleted widget
        deletedWidgets.forEach((widget: FlattenedWidgetProps) => {
          // Add it to the widgets list we fetched from reducer
          widgets[widget.widgetId] = widget;
          // If the widget in question is the deleted widget
          if (deletedWidgetIds.includes(widget.widgetId)) {
            //SPECIAL HANDLING FOR TAB IN A TABS WIDGET
            if (
              widget.tabId &&
              widget.type === WidgetTypes.CANVAS_WIDGET &&
              widget.parentId
            ) {
              const parent = cloneDeep(widgets[widget.parentId]);
              if (parent.tabsObj) {
                try {
                  const tabs = Object.values(parent.tabsObj);
                  parent.tabsObj[widget.tabId] = {
                    id: widget.tabId,
                    widgetId: widget.widgetId,
                    label: widget.tabName || widget.widgetName,
                    isVisible: true,
                  };
                  widgets = {
                    ...widgets,
                    [widget.parentId]: {
                      ...widgets[widget.parentId],
                      tabsObj: parent.tabsObj,
                    },
                  };
                } catch (error) {
                  log.debug("Error deleting tabs widget: ", { error });
                }
              } else {
                parent.tabs = JSON.stringify([
                  {
                    id: widget.tabId,
                    widgetId: widget.widgetId,
                    label: widget.tabName || widget.widgetName,
                  },
                ]);
                widgets = {
                  ...widgets,
                  [widget.parentId]: parent,
                };
              }
            }
            let newChildren = [widget.widgetId];
            if (widget.parentId && widgets[widget.parentId].children) {
              // Concatenate the list of parents children with the current widgetId
              newChildren = newChildren.concat(
                widgets[widget.parentId].children,
              );
            }
            if (widget.parentId) {
              widgets = {
                ...widgets,
                [widget.parentId]: {
                  ...widgets[widget.parentId],
                  children: newChildren,
                },
              };
            }
          }
        });
        return widgets;
      },
      stateWidgets,
    );
    const parentId = deletedWidgets[0].parentId;
    if (parentId) {
      resizeCanvasToLowestWidget(finalWidgets, parentId);
    }
    yield put(updateAndSaveLayout(finalWidgets));
    deletedWidgetIds.forEach((widgetId) => {
      setTimeout(() => flashElementById(widgetId), 100);
    });
    yield put(selectMultipleWidgetsInitAction(deletedWidgetIds));
    if (deletedWidgetIds.length === 1) {
      yield put(forceOpenPropertyPane(action.payload.widgetId));
    }
    yield flushDeletedWidgets(action.payload.widgetId);
  }
}

export function* moveSaga(moveAction: ReduxAction<WidgetMove>) {
  try {
    Toaster.clear();
    const start = performance.now();
    const {
      leftColumn,
      newParentId,
      parentId,
      topRow,
      widgetId,
    } = moveAction.payload;
    const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId);
    let widget = Object.assign({}, stateWidget);
    // Get all widgets from DSL/Redux Store
    const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
    const widgets = Object.assign({}, stateWidgets);
    // Get parent from DSL/Redux Store
    const stateParent: FlattenedWidgetProps = yield select(getWidget, parentId);
    const parent = {
      ...stateParent,
      children: [...(stateParent.children || [])],
    };
    // Update position of widget
    const updatedPosition = updateWidgetPosition(widget, leftColumn, topRow);
    widget = { ...widget, ...updatedPosition };

    // Replace widget with update widget props
    widgets[widgetId] = widget;
    // If the parent has changed i.e parentWidgetId is not parent.widgetId
    if (parent.widgetId !== newParentId && widgetId !== newParentId) {
      // Remove from the previous parent

      if (parent.children && Array.isArray(parent.children)) {
        const indexOfChild = parent.children.indexOf(widgetId);
        if (indexOfChild > -1) delete parent.children[indexOfChild];
        parent.children = parent.children.filter(Boolean);
      }

      // Add to new parent

      widgets[parent.widgetId] = parent;
      const newParent = {
        ...widgets[newParentId],
        children: widgets[newParentId].children
          ? [...(widgets[newParentId].children || []), widgetId]
          : [widgetId],
      };
      widgets[widgetId].parentId = newParentId;
      widgets[newParentId] = newParent;
    }
    log.debug("move computations took", performance.now() - start, "ms");

    yield put(updateAndSaveLayout(widgets));
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
      payload: {
        action: ReduxActionTypes.WIDGET_MOVE,
        error,
      },
    });
  }
}

export function* resizeSaga(resizeAction: ReduxAction<WidgetResize>) {
  try {
    Toaster.clear();
    const start = performance.now();
    const {
      bottomRow,
      leftColumn,
      rightColumn,
      topRow,
      widgetId,
    } = resizeAction.payload;

    const stateWidget: FlattenedWidgetProps = yield select(getWidget, widgetId);
    let widget = { ...stateWidget };
    const stateWidgets = yield select(getWidgets);
    const widgets = { ...stateWidgets };

    widget = { ...widget, leftColumn, rightColumn, topRow, bottomRow };
    widgets[widgetId] = widget;
    log.debug("resize computations took", performance.now() - start, "ms");
    yield put(updateAndSaveLayout(widgets));
  } catch (error) {
    yield put({
      type: ReduxActionErrorTypes.WIDGET_OPERATION_ERROR,
      payload: {
        action: ReduxActionTypes.WIDGET_RESIZE,
        error,
      },
    });
  }
}

enum DynamicPathUpdateEffectEnum {
  ADD = "ADD",
  REMOVE = "REMOVE",
  NOOP = "NOOP",
}

type DynamicPathUpdate = {
  propertyPath: string;
  effect: DynamicPathUpdateEffectEnum;
};

function getDynamicTriggerPathListUpdate(
  widget: WidgetProps,
  propertyPath: string,
  propertyValue: string,
): DynamicPathUpdate {
  if (propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
    return {
      propertyPath,
      effect: DynamicPathUpdateEffectEnum.ADD,
    };
  } else if (!propertyValue && !isPathADynamicTrigger(widget, propertyPath)) {
    return {
      propertyPath,
      effect: DynamicPathUpdateEffectEnum.REMOVE,
    };
  }
  return {
    propertyPath,
    effect: DynamicPathUpdateEffectEnum.NOOP,
  };
}

function getDynamicBindingPathListUpdate(
  widget: WidgetProps,
  propertyPath: string,
  propertyValue: any,
): DynamicPathUpdate {
  let stringProp = propertyValue;
  if (_.isObject(propertyValue)) {
    // Stringify this because composite controls may have bindings in the sub controls
    stringProp = JSON.stringify(propertyValue);
  }

  //TODO(abhinav): This is not appropriate from the platform's archtecture's point of view.
  // Figure out a holistic solutions where we donot have to stringify above.
  if (propertyPath === "primaryColumns" || propertyPath === "derivedColumns") {
    return {
      propertyPath,
      effect: DynamicPathUpdateEffectEnum.NOOP,
    };
  }

  const isDynamic = isDynamicValue(stringProp);
  if (!isDynamic && isPathADynamicBinding(widget, propertyPath)) {
    return {
      propertyPath,
      effect: DynamicPathUpdateEffectEnum.REMOVE,
    };
  } else if (isDynamic && !isPathADynamicBinding(widget, propertyPath)) {
    return {
      propertyPath,
      effect: DynamicPathUpdateEffectEnum.ADD,
    };
  }
  return {
    propertyPath,
    effect: DynamicPathUpdateEffectEnum.NOOP,
  };
}

function applyDynamicPathUpdates(
  currentList: DynamicPath[],
  update: DynamicPathUpdate,
): DynamicPath[] {
  if (update.effect === DynamicPathUpdateEffectEnum.ADD) {
    currentList.push({
      key: update.propertyPath,
    });
  } else if (update.effect === DynamicPathUpdateEffectEnum.REMOVE) {
    currentList = _.reject(currentList, { key: update.propertyPath });
  }
  return currentList;
}

const isPropertyATriggerPath = (
  widget: WidgetProps,
  propertyPath: string,
): boolean => {
  const widgetConfig = WidgetFactory.getWidgetPropertyPaneConfig(widget.type);
  const { triggerPaths } = getAllPathsFromPropertyConfig(
    widget,
    widgetConfig,
    {},
  );
  return propertyPath in triggerPaths;
};

function* updateWidgetPropertySaga(
  updateAction: ReduxAction<UpdateWidgetPropertyRequestPayload>,
) {
  const {
    payload: { propertyPath, propertyValue, widgetId },
  } = updateAction;

  // Holder object to collect all updates
  const updates: Record<string, unknown> = {
    [propertyPath]: propertyValue,
  };
  // Push these updates via the batch update
  yield call(
    batchUpdateWidgetPropertySaga,
    batchUpdateWidgetProperty(widgetId, { modify: updates }),
  );
}

function* setWidgetDynamicPropertySaga(
  action: ReduxAction<SetWidgetDynamicPropertyPayload>,
) {
  const { isDynamic, propertyPath, widgetId } = action.payload;
  const stateWidget: WidgetProps = yield select(getWidget, widgetId);
  let widget = cloneDeep({ ...stateWidget });
  const propertyValue = _.get(widget, propertyPath);

  let dynamicPropertyPathList = getWidgetDynamicPropertyPathList(widget);
  if (isDynamic) {
    const keyExists =
      dynamicPropertyPathList.findIndex((path) => path.key === propertyPath) >
      -1;
    if (!keyExists) {
      dynamicPropertyPathList.push({
        key: propertyPath,
      });
    }
    widget = set(widget, propertyPath, convertToString(propertyValue));
  } else {
    dynamicPropertyPathList = _.reject(dynamicPropertyPathList, {
      key: propertyPath,
    });
    const { parsed } = yield call(
      validateProperty,
      propertyPath,
      propertyValue,
      widget,
    );
    widget = set(widget, propertyPath, parsed);
  }
  widget.dynamicPropertyPathList = dynamicPropertyPathList;

  const stateWidgets = yield select(getWidgets);
  const widgets = { ...stateWidgets, [widgetId]: widget };

  // Save the layout
  yield put(updateAndSaveLayout(widgets));
}

function getPropertiesToUpdate(
  widget: WidgetProps,
  updates: Record<string, unknown>,
  triggerPaths?: string[],
): {
  propertyUpdates: Record<string, unknown>;
  dynamicTriggerPathList: DynamicPath[];
  dynamicBindingPathList: DynamicPath[];
} {
  // Create a
  const widgetWithUpdates = _.cloneDeep(widget);
  Object.entries(updates).forEach(([propertyPath, propertyValue]) => {
    set(widgetWithUpdates, propertyPath, propertyValue);
  });

  // get the flat list of all updates (in case values are objects)
  const updatePaths = getAllPaths(updates);

  const propertyUpdates: Record<string, unknown> = {
    ...updates,
  };
  const currentDynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
    widget,
  );
  const currentDynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList(
    widget,
  );
  const dynamicTriggerPathListUpdates: DynamicPathUpdate[] = [];
  const dynamicBindingPathListUpdates: DynamicPathUpdate[] = [];

  Object.keys(updatePaths).forEach((propertyPath) => {
    const propertyValue = _.get(updates, propertyPath);
    // only check if
    if (!_.isString(propertyValue)) {
      return;
    }

    // Check if the path is a of a dynamic trigger property
    let isTriggerProperty = isPropertyATriggerPath(
      widgetWithUpdates,
      propertyPath,
    );

    isTriggerProperty = doesTriggerPathsContainPropertyPath(
      isTriggerProperty,
      propertyPath,
      triggerPaths,
    );

    // If it is a trigger property, it will go in a different list than the general
    // dynamicBindingPathList.
    if (isTriggerProperty) {
      dynamicTriggerPathListUpdates.push(
        getDynamicTriggerPathListUpdate(widget, propertyPath, propertyValue),
      );
    } else {
      dynamicBindingPathListUpdates.push(
        getDynamicBindingPathListUpdate(widget, propertyPath, propertyValue),
      );
    }
  });

  const dynamicTriggerPathList = dynamicTriggerPathListUpdates.reduce(
    applyDynamicPathUpdates,
    currentDynamicTriggerPathList,
  );
  const dynamicBindingPathList = dynamicBindingPathListUpdates.reduce(
    applyDynamicPathUpdates,
    currentDynamicBindingPathList,
  );

  return {
    propertyUpdates,
    dynamicTriggerPathList,
    dynamicBindingPathList,
  };
}

function* batchUpdateWidgetPropertySaga(
  action: ReduxAction<UpdateWidgetPropertyPayload>,
) {
  const start = performance.now();
  const { updates, widgetId } = action.payload;
  if (!widgetId) {
    // Handling the case where sometimes widget id is not passed through here
    return;
  }
  const { modify = {}, remove = [], triggerPaths } = updates;

  const stateWidget: WidgetProps = yield select(getWidget, widgetId);

  // if there is no widget in the state, don't do anything
  if (!stateWidget) return;

  let widget = cloneDeep(stateWidget);
  try {
    if (Object.keys(modify).length > 0) {
      const {
        dynamicBindingPathList,
        dynamicTriggerPathList,
        propertyUpdates,
      } = getPropertiesToUpdate(widget, modify, triggerPaths);

      // We loop over all updates
      Object.entries(propertyUpdates).forEach(
        ([propertyPath, propertyValue]) => {
          // since property paths could be nested, we use lodash set method
          widget = set(widget, propertyPath, propertyValue);
        },
      );
      widget.dynamicBindingPathList = dynamicBindingPathList;
      widget.dynamicTriggerPathList = dynamicTriggerPathList;
    }
  } catch (e) {
    log.debug("Error updating property paths: ", { e });
  }

  if (Array.isArray(remove) && remove.length > 0) {
    widget = yield removeWidgetProperties(widget, remove);
  }

  const stateWidgets = yield select(getWidgets);
  const widgets = { ...stateWidgets, [widgetId]: widget };
  log.debug(
    "Batch widget property update calculations took: ",
    performance.now() - start,
    "ms",
  );

  // Save the layout
  yield put(updateAndSaveLayout(widgets));
}

function* removeWidgetProperties(widget: WidgetProps, paths: string[]) {
  try {
    let dynamicTriggerPathList: DynamicPath[] = getWidgetDynamicTriggerPathList(
      widget,
    );
    let dynamicBindingPathList: DynamicPath[] = getEntityDynamicBindingPathList(
      widget,
    );
    let dynamicPropertyPathList: DynamicPath[] = getWidgetDynamicPropertyPathList(
      widget,
    );

    paths.forEach((propertyPath) => {
      dynamicTriggerPathList = dynamicTriggerPathList.filter((dynamicPath) => {
        return !isChildPropertyPath(propertyPath, dynamicPath.key);
      });

      dynamicBindingPathList = dynamicBindingPathList.filter((dynamicPath) => {
        return !isChildPropertyPath(propertyPath, dynamicPath.key);
      });

      dynamicPropertyPathList = dynamicPropertyPathList.filter(
        (dynamicPath) => {
          return !isChildPropertyPath(propertyPath, dynamicPath.key);
        },
      );
    });

    widget.dynamicBindingPathList = dynamicBindingPathList;
    widget.dynamicTriggerPathList = dynamicTriggerPathList;
    widget.dynamicPropertyPathList = dynamicPropertyPathList;

    paths.forEach((propertyPath) => {
      widget = unsetPropertyPath(widget, propertyPath) as WidgetProps;
    });
  } catch (e) {
    log.debug("Error removing propertyPaths: ", { e });
  }

  return widget;
}

function* deleteWidgetPropertySaga(
  action: ReduxAction<DeleteWidgetPropertyPayload>,
) {
  const { propertyPaths, widgetId } = action.payload;
  if (!widgetId) {
    // Handling the case where sometimes widget id is not passed through here
    return;
  }

  yield put(batchUpdateWidgetProperty(widgetId, { remove: propertyPaths }));
}

//TODO(abhinav): Move this to helpers and add tests
const unsetPropertyPath = (obj: Record<string, unknown>, path: string) => {
  const regex = /(.*)\[\d+\]$/;
  if (regex.test(path)) {
    const matches = path.match(regex);
    if (
      matches &&
      Array.isArray(matches) &&
      matches[1] &&
      matches[1].length > 0
    ) {
      _.unset(obj, path);
      const arr = _.get(obj, matches[1]);
      if (arr && Array.isArray(arr)) {
        _.set(obj, matches[1], arr.filter(Boolean));
      }
    }
  } else {
    _.unset(obj, path);
  }
  return obj;
};

function* resetChildrenMetaSaga(action: ReduxAction<{ widgetId: string }>) {
  const parentWidgetId = action.payload.widgetId;
  const canvasWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
  const childrenIds: string[] = getWidgetChildren(
    canvasWidgets,
    parentWidgetId,
  );
  for (const childIndex in childrenIds) {
    const childId = childrenIds[childIndex];
    yield put(resetWidgetMetaProperty(childId));
  }
}

function* updateCanvasSize(
  action: ReduxAction<{ canvasWidgetId: string; snapRows: number }>,
) {
  const { canvasWidgetId, snapRows } = action.payload;
  const canvasWidget = yield select(getWidget, canvasWidgetId);

  const originalSnapRows = canvasWidget.bottomRow - canvasWidget.topRow;

  const newBottomRow = Math.round(
    snapRows * GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
  );
  /* Update the canvas's rows, ONLY if it has changed since the last render */
  if (originalSnapRows !== newBottomRow) {
    // TODO(abhinav): This considers that the topRow will always be zero
    // Check this out when non canvas widgets are updating snapRows
    // erstwhile: Math.round((rows * props.snapRowSpace) / props.parentRowSpace),
    yield put(
      batchUpdateWidgetProperty(canvasWidgetId, {
        modify: { bottomRow: newBottomRow },
      }),
    );
  }
}

function* createWidgetCopy(widget: FlattenedWidgetProps) {
  const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
    getWidgets,
  );
  const widgetsToStore = getAllWidgetsInTree(widget.widgetId, allWidgets);
  return {
    widgetId: widget.widgetId,
    list: widgetsToStore,
    parentId: widget.parentId,
  };
}

function* createSelectedWidgetsCopy(selectedWidgets: FlattenedWidgetProps[]) {
  if (!selectedWidgets || !selectedWidgets.length) return;
  const widgetListsToStore: {
    widgetId: string;
    parentId: string;
    list: FlattenedWidgetProps[];
  }[] = yield all(selectedWidgets.map((each) => call(createWidgetCopy, each)));
  return yield saveCopiedWidgets(JSON.stringify(widgetListsToStore));
}

/**
 * copy here actually means saving a JSON in local storage
 * so when a user hits copy on a selected widget, we save widget in localStorage
 *
 * @param action
 * @returns
 */
function* copyWidgetSaga(action: ReduxAction<{ isShortcut: boolean }>) {
  const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
    getWidgets,
  );
  const selectedWidgets: string[] = yield select(getSelectedWidgets);
  if (!selectedWidgets) {
    Toaster.show({
      text: createMessage(ERROR_WIDGET_COPY_NO_WIDGET_SELECTED),
      variant: Variant.info,
    });
    return;
  }

  const allAllowedToCopy = selectedWidgets.some((each) => {
    return !allWidgets[each].disallowCopy;
  });

  if (!allAllowedToCopy) {
    Toaster.show({
      text: createMessage(ERROR_WIDGET_COPY_NOT_ALLOWED),
      variant: Variant.info,
    });

    return;
  }
  const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);

  const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps);

  selectedWidgetProps.forEach((each) => {
    const eventName = action.payload.isShortcut
      ? "WIDGET_COPY_VIA_SHORTCUT"
      : "WIDGET_COPY";
    AnalyticsUtil.logEvent(eventName, {
      widgetName: each.widgetName,
      widgetType: each.type,
    });
  });

  if (saveResult) {
    Toaster.show({
      text: createMessage(
        WIDGET_COPY,
        selectedWidgetProps.length > 1
          ? `${selectedWidgetProps.length} Widgets`
          : selectedWidgetProps[0].widgetName,
      ),
      variant: Variant.success,
    });
  }
}

export function calculateNewWidgetPosition(
  widget: WidgetProps,
  parentId: string,
  canvasWidgets: { [widgetId: string]: FlattenedWidgetProps },
  parentBottomRow?: number,
  persistColumnPosition = false,
) {
  // Note: This is a very simple algorithm.
  // We take the bottom most widget in the canvas, then calculate the top,left,right,bottom
  // co-ordinates for the new widget, such that it can be placed at the bottom of the canvas.
  const nextAvailableRow = parentBottomRow
    ? parentBottomRow
    : nextAvailableRowInContainer(parentId, canvasWidgets);
  return {
    leftColumn: persistColumnPosition ? widget.leftColumn : 0,
    rightColumn: persistColumnPosition
      ? widget.rightColumn
      : widget.rightColumn - widget.leftColumn,
    topRow: parentBottomRow
      ? nextAvailableRow + widget.topRow
      : nextAvailableRow,
    bottomRow: parentBottomRow
      ? nextAvailableRow + widget.bottomRow
      : nextAvailableRow + (widget.bottomRow - widget.topRow),
  };
}

function* getEntityNames() {
  const evalTree = yield select(getDataTree);
  return Object.keys(evalTree);
}

function getNextWidgetName(
  widgets: CanvasWidgetsReduxState,
  type: WidgetType,
  evalTree: {
    bottomRow: any;
    leftColumn: any;
    rightColumn: any;
    topRow: any;
  },
  options?: Record<string, unknown>,
) {
  // Compute the new widget's name
  const defaultConfig: any = WidgetConfigResponse.config[type];
  const widgetNames = Object.keys(widgets).map((w) => widgets[w].widgetName);
  const entityNames = Object.keys(evalTree);
  let prefix = defaultConfig.widgetName;
  if (options && options.prefix) {
    prefix = `${options.prefix}${
      widgetNames.indexOf(options.prefix as string) > -1 ? "Copy" : ""
    }`;
  }

  return getNextEntityName(
    prefix,
    [...widgetNames, ...entityNames],
    options?.startWithoutIndex as boolean,
  );
}

/**
 * this saga create a new widget from the copied one to store
 */
function* pasteWidgetSaga() {
  const copiedWidgetGroups: {
    widgetId: string;
    parentId: string;
    list: WidgetProps[];
  }[] = yield getCopiedWidgets();

  if (!Array.isArray(copiedWidgetGroups)) {
    return;
    // to avoid invoking old copied widgets
  }
  const stateWidgets: CanvasWidgetsReduxState = yield select(getWidgets);
  let selectedWidget: FlattenedWidgetProps | undefined = yield select(
    getSelectedWidget,
  );
  const focusedWidget: FlattenedWidgetProps | undefined = yield select(
    getFocusedWidget,
  );

  selectedWidget = yield checkIfPastingIntoListWidget(
    stateWidgets,
    selectedWidget || focusedWidget,
    copiedWidgetGroups,
  );

  const pastingIntoWidgetId: string = yield getParentWidgetIdForPasting(
    { ...stateWidgets },
    selectedWidget,
  );

  let widgets = { ...stateWidgets };
  const newlyCreatedWidgetIds: string[] = [];
  const sortedWidgetList = copiedWidgetGroups.sort(
    (a, b) => a.list[0].topRow - b.list[0].topRow,
  );
  const copiedGroupTopRow = sortedWidgetList[0].list[0].topRow;
  const nextAvailableRow: number = nextAvailableRowInContainer(
    pastingIntoWidgetId,
    widgets,
  );
  yield all(
    copiedWidgetGroups.map((copiedWidgets) =>
      call(function*() {
        // Don't try to paste if there is no copied widget
        if (!copiedWidgets) return;
        const copiedWidgetId = copiedWidgets.widgetId;
        const unUpdatedCopyOfWidget = copiedWidgets.list.find(
          (widget) => widget.widgetId === copiedWidgetId,
        );

        if (unUpdatedCopyOfWidget) {
          const copiedWidget = {
            ...unUpdatedCopyOfWidget,
            topRow: unUpdatedCopyOfWidget.topRow - copiedGroupTopRow,
            bottomRow: unUpdatedCopyOfWidget.bottomRow - copiedGroupTopRow,
          };

          // Log the paste event
          AnalyticsUtil.logEvent("WIDGET_PASTE", {
            widgetName: copiedWidget.widgetName,
            widgetType: copiedWidget.type,
          });

          // Compute the new widget's positional properties
          const {
            bottomRow,
            leftColumn,
            rightColumn,
            topRow,
          } = yield calculateNewWidgetPosition(
            copiedWidget,
            pastingIntoWidgetId,
            widgets,
            nextAvailableRow,
            true,
          );
          // goToNextAvailableRow = true,
          // persistColumnPosition = false,

          const evalTree = yield select(getDataTree);

          // Get a flat list of all the widgets to be updated
          const widgetList = copiedWidgets.list;
          const widgetIdMap: Record<string, string> = {};
          const widgetNameMap: Record<string, string> = {};
          const newWidgetList: FlattenedWidgetProps[] = [];
          let newWidgetId: string = copiedWidget.widgetId;
          // Generate new widgetIds for the flat list of all the widgets to be updated
          widgetList.forEach((widget) => {
            // Create a copy of the widget properties
            const newWidget = cloneDeep(widget);
            newWidget.widgetId = generateReactKey();
            // Add the new widget id so that it maps the previous widget id
            widgetIdMap[widget.widgetId] = newWidget.widgetId;

            // Add the new widget to the list
            newWidgetList.push(newWidget);
          });

          // For each of the new widgets generated
          for (let i = 0; i < newWidgetList.length; i++) {
            const widget = newWidgetList[i];
            const oldWidgetName = widget.widgetName;
            // Generate a new unique widget name
            const newWidgetName = getNextWidgetName(
              widgets,
              widget.type,
              evalTree,
              {
                prefix: oldWidgetName,
                startWithoutIndex: true,
              },
            );
            // Update the children widgetIds if it has children
            if (widget.children && widget.children.length > 0) {
              widget.children.forEach(
                (childWidgetId: string, index: number) => {
                  if (widget.children) {
                    widget.children[index] = widgetIdMap[childWidgetId];
                  }
                },
              );
            }

            // Update the tabs for the tabs widget.
            if (widget.tabsObj && widget.type === WidgetTypes.TABS_WIDGET) {
              try {
                const tabs = Object.values(widget.tabsObj);
                if (Array.isArray(tabs)) {
                  widget.tabsObj = tabs.reduce((obj: any, tab) => {
                    tab.widgetId = widgetIdMap[tab.widgetId];
                    obj[tab.id] = tab;
                    return obj;
                  }, {});
                }
              } catch (error) {
                log.debug("Error updating tabs", error);
              }
            }

            // Update the table widget column properties
            if (widget.type === WidgetTypes.TABLE_WIDGET) {
              try {
                // If the primaryColumns of the table exist
                if (widget.primaryColumns) {
                  // For each column
                  for (const [columnId, column] of Object.entries(
                    widget.primaryColumns,
                  )) {
                    // For each property in the column
                    for (const [key, value] of Object.entries(
                      column as ColumnProperties,
                    )) {
                      // Replace reference of previous widget with the new widgetName
                      // This handles binding scenarios like `{{Table2.tableData.map((currentRow) => (currentRow.id))}}`
                      widget.primaryColumns[columnId][key] = isString(value)
                        ? value.replace(
                            `${oldWidgetName}.`,
                            `${newWidgetName}.`,
                          )
                        : value;
                    }
                  }
                }
                // Use the new widget name we used to replace the column properties above.
                widget.widgetName = newWidgetName;
              } catch (error) {
                log.debug("Error updating table widget properties", error);
              }
            }

            // If it is the copied widget, update position properties
            if (widget.widgetId === widgetIdMap[copiedWidget.widgetId]) {
              newWidgetId = widget.widgetId;
              widget.leftColumn = leftColumn;
              widget.topRow = topRow;
              widget.bottomRow = bottomRow;
              widget.rightColumn = rightColumn;
              widget.parentId = pastingIntoWidgetId;
              // Also, update the parent widget in the canvas widgets
              // to include this new copied widget's id in the parent's children
              let parentChildren = [widget.widgetId];
              const widgetChildren = widgets[pastingIntoWidgetId].children;
              if (widgetChildren && Array.isArray(widgetChildren)) {
                // Add the new child to existing children
                parentChildren = parentChildren.concat(widgetChildren);
              }
              const updateBottomRow =
                widget.bottomRow * widget.parentRowSpace >
                widgets[pastingIntoWidgetId].bottomRow;
              widgets = {
                ...widgets,
                [pastingIntoWidgetId]: {
                  ...widgets[pastingIntoWidgetId],
                  ...(updateBottomRow
                    ? {
                        bottomRow: widget.bottomRow * widget.parentRowSpace,
                      }
                    : {}),
                  children: parentChildren,
                },
              };
              // If the copied widget's boundaries exceed the parent's
              // Make the parent scrollable
              if (
                widgets[pastingIntoWidgetId].bottomRow *
                  widgets[widget.parentId].parentRowSpace <=
                widget.bottomRow * widget.parentRowSpace
              ) {
                const parentOfPastingWidget =
                  widgets[pastingIntoWidgetId].parentId;
                if (
                  parentOfPastingWidget &&
                  widget.parentId !== MAIN_CONTAINER_WIDGET_ID
                ) {
                  const parent = widgets[parentOfPastingWidget];
                  widgets[parentOfPastingWidget] = {
                    ...parent,
                    shouldScrollContents: true,
                  };
                }
              }
            } else {
              // For all other widgets in the list
              // (These widgets will be descendants of the copied widget)
              // This means, that their parents will also be newly copied widgets
              // Update widget's parent widget ids with the new parent widget ids
              const newParentId = newWidgetList.find((newWidget) =>
                widget.parentId
                  ? newWidget.widgetId === widgetIdMap[widget.parentId]
                  : false,
              )?.widgetId;
              if (newParentId) widget.parentId = newParentId;
            }
            widget.widgetName = newWidgetName;
            widgetNameMap[oldWidgetName] = widget.widgetName;
            // Add the new widget to the canvas widgets
            widgets[widget.widgetId] = widget;
          }
          newlyCreatedWidgetIds.push(widgetIdMap[copiedWidgetId]);
          // 1. updating template in the copied widget and deleting old template associations
          // 2. updating dynamicBindingPathList in the copied grid widget
          for (let i = 0; i < newWidgetList.length; i++) {
            const widget = newWidgetList[i];

            widgets = handleSpecificCasesWhilePasting(
              widget,
              widgets,
              widgetNameMap,
              newWidgetList,
            );
          }
        }
      }),
    ),
  );
  // save the new DSL
  yield put(updateAndSaveLayout(widgets));
  newlyCreatedWidgetIds.forEach((newWidgetId) => {
    setTimeout(() => flashElementById(newWidgetId), 100);
  });
  // hydrating enhancements map after save layout so that enhancement map
  // for newly copied widget is hydrated
  yield put(selectMultipleWidgetsInitAction(newlyCreatedWidgetIds));
}

function* cutWidgetSaga() {
  const allWidgets: { [widgetId: string]: FlattenedWidgetProps } = yield select(
    getWidgets,
  );
  const selectedWidgets: string[] = yield select(getSelectedWidgets);
  if (!selectedWidgets) {
    Toaster.show({
      text: createMessage(ERROR_WIDGET_CUT_NO_WIDGET_SELECTED),
      variant: Variant.info,
    });
    return;
  }

  const selectedWidgetProps = selectedWidgets.map((each) => allWidgets[each]);

  const saveResult = yield createSelectedWidgetsCopy(selectedWidgetProps);

  selectedWidgetProps.forEach((each) => {
    const eventName = "WIDGET_CUT_VIA_SHORTCUT"; // cut only supported through a shortcut
    AnalyticsUtil.logEvent(eventName, {
      widgetName: each.widgetName,
      widgetType: each.type,
    });
  });

  if (saveResult) {
    Toaster.show({
      text: createMessage(
        WIDGET_CUT,
        selectedWidgetProps.length > 1
          ? `${selectedWidgetProps.length} Widgets`
          : selectedWidgetProps[0].widgetName,
      ),
      variant: Variant.success,
    });
  }

  yield put({
    type: ReduxActionTypes.WIDGET_DELETE,
    payload: {
      disallowUndo: true,
      isShortcut: true,
    },
  });
}

function* addTableWidgetFromQuerySaga(action: ReduxAction<string>) {
  try {
    const columns = 8 * GRID_DENSITY_MIGRATION_V1;
    const rows = 7 * GRID_DENSITY_MIGRATION_V1;
    const queryName = action.payload;
    const widgets = yield select(getWidgets);
    const evalTree = yield select(getDataTree);
    const widgetName = getNextWidgetName(widgets, "TABLE_WIDGET", evalTree);

    let newWidget = {
      type: WidgetTypes.TABLE_WIDGET,
      newWidgetId: generateReactKey(),
      widgetId: "0",
      topRow: 0,
      bottomRow: rows,
      leftColumn: 0,
      rightColumn: columns,
      columns,
      rows,
      parentId: MAIN_CONTAINER_WIDGET_ID,
      widgetName,
      renderMode: RenderModes.CANVAS,
      parentRowSpace: GridDefaults.DEFAULT_GRID_ROW_HEIGHT,
      parentColumnSpace: 1,
      isLoading: false,
      version: 1,
      props: {
        tableData: `{{${queryName}.data}}`,
        dynamicBindingPathList: [{ key: "tableData" }],
      },
    };
    const {
      bottomRow,
      leftColumn,
      rightColumn,
      topRow,
    } = yield calculateNewWidgetPosition(
      newWidget,
      MAIN_CONTAINER_WIDGET_ID,
      widgets,
    );

    newWidget = {
      ...newWidget,
      leftColumn,
      topRow,
      rightColumn,
      bottomRow,
    };

    yield put({
      type: ReduxActionTypes.WIDGET_ADD_CHILD,
      payload: newWidget,
    });

    const applicationId = yield select(getCurrentApplicationId);
    const pageId = yield select(getCurrentPageId);

    navigateToCanvas(
      {
        applicationId,
        pageId,
      },
      window.location.pathname,
      pageId,
      newWidget.newWidgetId,
    );
    yield put({
      type: ReduxActionTypes.SELECT_WIDGET_INIT,
      payload: { widgetId: newWidget.newWidgetId },
    });
    yield put(forceOpenPropertyPane(newWidget.newWidgetId));
  } catch (error) {
    Toaster.show({
      text: createMessage(ERROR_ADD_WIDGET_FROM_QUERY),
      variant: Variant.danger,
    });
  }
}

export default function* widgetOperationSagas() {
  yield fork(widgetSelectionSagas);
  yield all([
    takeEvery(
      ReduxActionTypes.ADD_TABLE_WIDGET_FROM_QUERY,
      addTableWidgetFromQuerySaga,
    ),
    takeEvery(ReduxActionTypes.WIDGET_ADD_CHILD, addChildSaga),
    takeEvery(ReduxActionTypes.WIDGET_DELETE, deleteSagaInit),
    takeEvery(ReduxActionTypes.WIDGET_SINGLE_DELETE, deleteSaga),
    takeEvery(
      ReduxActionTypes.WIDGET_BULK_DELETE,
      deleteAllSelectedWidgetsSaga,
    ),
    takeLatest(ReduxActionTypes.WIDGET_MOVE, moveSaga),
    takeLatest(ReduxActionTypes.WIDGET_RESIZE, resizeSaga),
    takeEvery(
      ReduxActionTypes.UPDATE_WIDGET_PROPERTY_REQUEST,
      updateWidgetPropertySaga,
    ),
    takeEvery(
      ReduxActionTypes.WIDGET_UPDATE_PROPERTY,
      updateWidgetPropertySaga,
    ),
    takeEvery(
      ReduxActionTypes.SET_WIDGET_DYNAMIC_PROPERTY,
      setWidgetDynamicPropertySaga,
    ),
    takeEvery(
      ReduxActionTypes.RESET_CHILDREN_WIDGET_META,
      resetChildrenMetaSaga,
    ),
    takeEvery(
      ReduxActionTypes.BATCH_UPDATE_WIDGET_PROPERTY,
      batchUpdateWidgetPropertySaga,
    ),
    takeEvery(
      ReduxActionTypes.DELETE_WIDGET_PROPERTY,
      deleteWidgetPropertySaga,
    ),
    takeLatest(ReduxActionTypes.UPDATE_CANVAS_SIZE, updateCanvasSize),
    takeLatest(ReduxActionTypes.COPY_SELECTED_WIDGET_INIT, copyWidgetSaga),
    takeEvery(ReduxActionTypes.PASTE_COPIED_WIDGET_INIT, pasteWidgetSaga),
    takeEvery(ReduxActionTypes.UNDO_DELETE_WIDGET, undoDeleteSaga),
    takeEvery(ReduxActionTypes.CUT_SELECTED_WIDGET, cutWidgetSaga),
    takeEvery(ReduxActionTypes.WIDGET_ADD_CHILDREN, addChildrenSaga),
  ]);
}
