import _extends from "@babel/runtime/helpers/esm/extends";
import { gridPinnedRowsSelector } from './gridRowsSelector';
import { gridDensityFactorSelector } from '../density/densitySelector';
export const GRID_ROOT_GROUP_ID = `auto-generated-group-node-root`;
export const buildRootGroup = () => ({
  type: 'group',
  id: GRID_ROOT_GROUP_ID,
  depth: -1,
  groupingField: null,
  groupingKey: null,
  isAutoGenerated: true,
  children: [],
  childrenFromPath: {},
  childrenExpanded: true,
  parent: null
});

/**
 * A helper function to check if the id provided is valid.
 * @param {GridRowId} id Id as [[GridRowId]].
 * @param {GridRowModel | Partial<GridRowModel>} row Row as [[GridRowModel]].
 * @param {string} detailErrorMessage A custom error message to display for invalid IDs
 */
export function checkGridRowIdIsValid(id, row, detailErrorMessage = 'A row was provided without id in the rows prop:') {
  if (id == null) {
    throw new Error(['MUI: The data grid component requires all rows to have a unique `id` property.', 'Alternatively, you can use the `getRowId` prop to specify a custom id for each row.', detailErrorMessage, JSON.stringify(row)].join('\n'));
  }
}
export const getRowIdFromRowModel = (rowModel, getRowId, detailErrorMessage) => {
  const id = getRowId ? getRowId(rowModel) : rowModel.id;
  checkGridRowIdIsValid(id, rowModel, detailErrorMessage);
  return id;
};
export const createRowsInternalCache = ({
  rows,
  getRowId,
  loading,
  rowCount
}) => {
  const updates = {
    type: 'full',
    rows: []
  };
  const dataRowIdToModelLookup = {};
  const dataRowIdToIdLookup = {};
  for (let i = 0; i < rows.length; i += 1) {
    const model = rows[i];
    const id = getRowIdFromRowModel(model, getRowId);
    dataRowIdToModelLookup[id] = model;
    dataRowIdToIdLookup[id] = id;
    updates.rows.push(id);
  }
  return {
    rowsBeforePartialUpdates: rows,
    loadingPropBeforePartialUpdates: loading,
    rowCountPropBeforePartialUpdates: rowCount,
    updates,
    dataRowIdToIdLookup,
    dataRowIdToModelLookup
  };
};
export const getTopLevelRowCount = ({
  tree,
  rowCountProp = 0
}) => {
  const rootGroupNode = tree[GRID_ROOT_GROUP_ID];
  return Math.max(rowCountProp, rootGroupNode.children.length + (rootGroupNode.footerId == null ? 0 : 1));
};
export const getRowsStateFromCache = ({
  apiRef,
  rowCountProp = 0,
  loadingProp,
  previousTree,
  previousTreeDepths
}) => {
  const cache = apiRef.current.caches.rows;

  // 1. Apply the "rowTreeCreation" family processing.
  const {
    tree: unProcessedTree,
    treeDepths: unProcessedTreeDepths,
    dataRowIds: unProcessedDataRowIds,
    groupingName
  } = apiRef.current.applyStrategyProcessor('rowTreeCreation', {
    previousTree,
    previousTreeDepths,
    updates: cache.updates,
    dataRowIdToIdLookup: cache.dataRowIdToIdLookup,
    dataRowIdToModelLookup: cache.dataRowIdToModelLookup
  });

  // 2. Apply the "hydrateRows" pipe-processing.
  const groupingParamsWithHydrateRows = apiRef.current.unstable_applyPipeProcessors('hydrateRows', {
    tree: unProcessedTree,
    treeDepths: unProcessedTreeDepths,
    dataRowIdToIdLookup: cache.dataRowIdToIdLookup,
    dataRowIds: unProcessedDataRowIds,
    dataRowIdToModelLookup: cache.dataRowIdToModelLookup
  });

  // 3. Reset the cache updates
  apiRef.current.caches.rows.updates = {
    type: 'partial',
    actions: {
      insert: [],
      modify: [],
      remove: []
    },
    idToActionLookup: {}
  };
  return _extends({}, groupingParamsWithHydrateRows, {
    totalRowCount: Math.max(rowCountProp, groupingParamsWithHydrateRows.dataRowIds.length),
    totalTopLevelRowCount: getTopLevelRowCount({
      tree: groupingParamsWithHydrateRows.tree,
      rowCountProp
    }),
    groupingName,
    loading: loadingProp
  });
};
export const isAutoGeneratedRow = rowNode => rowNode.type === 'skeletonRow' || rowNode.type === 'footer' || rowNode.type === 'group' && rowNode.isAutoGenerated || rowNode.type === 'pinnedRow' && rowNode.isAutoGenerated;
export const getTreeNodeDescendants = (tree, parentId, skipAutoGeneratedRows) => {
  const node = tree[parentId];
  if (node.type !== 'group') {
    return [];
  }
  const validDescendants = [];
  for (let i = 0; i < node.children.length; i += 1) {
    const child = node.children[i];
    if (!skipAutoGeneratedRows || !isAutoGeneratedRow(tree[child])) {
      validDescendants.push(child);
    }
    validDescendants.push(...getTreeNodeDescendants(tree, child, skipAutoGeneratedRows));
  }
  if (!skipAutoGeneratedRows && node.footerId != null) {
    validDescendants.push(node.footerId);
  }
  return validDescendants;
};
export const updateCacheWithNewRows = ({
  previousCache,
  getRowId,
  updates
}) => {
  if (previousCache.updates.type === 'full') {
    throw new Error('MUI: Unable to prepare a partial update if a full update is not applied yet');
  }

  // Remove duplicate updates.
  // A server can batch updates, and send several updates for the same row in one fn call.
  const uniqueUpdates = new Map();
  updates.forEach(update => {
    const id = getRowIdFromRowModel(update, getRowId, 'A row was provided without id when calling updateRows():');
    if (uniqueUpdates.has(id)) {
      uniqueUpdates.set(id, _extends({}, uniqueUpdates.get(id), update));
    } else {
      uniqueUpdates.set(id, update);
    }
  });
  const partialUpdates = {
    type: 'partial',
    actions: {
      insert: [...(previousCache.updates.actions.insert ?? [])],
      modify: [...(previousCache.updates.actions.modify ?? [])],
      remove: [...(previousCache.updates.actions.remove ?? [])]
    },
    idToActionLookup: _extends({}, previousCache.updates.idToActionLookup)
  };
  const dataRowIdToModelLookup = _extends({}, previousCache.dataRowIdToModelLookup);
  const dataRowIdToIdLookup = _extends({}, previousCache.dataRowIdToIdLookup);
  const alreadyAppliedActionsToRemove = {
    insert: {},
    modify: {},
    remove: {}
  };

  // Depending on the action already applied to the data row,
  // We might want drop the already-applied-update.
  // For instance:
  // - if you delete then insert, then you don't want to apply the deletion in the tree.
  // - if you insert, then modify, then you just want to apply the insertion in the tree.
  uniqueUpdates.forEach((partialRow, id) => {
    const actionAlreadyAppliedToRow = partialUpdates.idToActionLookup[id];

    // Action === "delete"
    // eslint-disable-next-line no-underscore-dangle
    if (partialRow._action === 'delete') {
      // If the data row has been removed since the last state update,
      // Then do nothing.
      if (actionAlreadyAppliedToRow === 'remove' || !dataRowIdToModelLookup[id]) {
        return;
      }

      // If the data row has been inserted / modified since the last state update,
      // Then drop this "insert" / "modify" update.
      if (actionAlreadyAppliedToRow != null) {
        alreadyAppliedActionsToRemove[actionAlreadyAppliedToRow][id] = true;
      }

      // Remove the data row from the lookups and add it to the "delete" update.
      partialUpdates.actions.remove.push(id);
      delete dataRowIdToModelLookup[id];
      delete dataRowIdToIdLookup[id];
      return;
    }
    const oldRow = dataRowIdToModelLookup[id];

    // Action === "modify"
    if (oldRow) {
      // If the data row has been removed since the last state update,
      // Then drop this "remove" update and add it to the "modify" update instead.
      if (actionAlreadyAppliedToRow === 'remove') {
        alreadyAppliedActionsToRemove.remove[id] = true;
        partialUpdates.actions.modify.push(id);
      }
      // If the date has not been inserted / modified since the last state update,
      // Then add it to the "modify" update (if it has been inserted it should just remain "inserted").
      else if (actionAlreadyAppliedToRow == null) {
        partialUpdates.actions.modify.push(id);
      }

      // Update the data row lookups.
      dataRowIdToModelLookup[id] = _extends({}, oldRow, partialRow);
      return;
    }

    // Action === "insert"
    // If the data row has been removed since the last state update,
    // Then drop the "remove" update and add it to the "insert" update instead.
    if (actionAlreadyAppliedToRow === 'remove') {
      alreadyAppliedActionsToRemove.remove[id] = true;
      partialUpdates.actions.insert.push(id);
    }
    // If the data row has not been inserted since the last state update,
    // Then add it to the "insert" update.
    // `actionAlreadyAppliedToRow` can't be equal to "modify", otherwise we would have an `oldRow` above.
    else if (actionAlreadyAppliedToRow == null) {
      partialUpdates.actions.insert.push(id);
    }

    // Update the data row lookups.
    dataRowIdToModelLookup[id] = partialRow;
    dataRowIdToIdLookup[id] = id;
  });
  const actionTypeWithActionsToRemove = Object.keys(alreadyAppliedActionsToRemove);
  for (let i = 0; i < actionTypeWithActionsToRemove.length; i += 1) {
    const actionType = actionTypeWithActionsToRemove[i];
    const idsToRemove = alreadyAppliedActionsToRemove[actionType];
    if (Object.keys(idsToRemove).length > 0) {
      partialUpdates.actions[actionType] = partialUpdates.actions[actionType].filter(id => !idsToRemove[id]);
    }
  }
  return {
    dataRowIdToModelLookup,
    dataRowIdToIdLookup,
    updates: partialUpdates,
    rowsBeforePartialUpdates: previousCache.rowsBeforePartialUpdates,
    loadingPropBeforePartialUpdates: previousCache.loadingPropBeforePartialUpdates,
    rowCountPropBeforePartialUpdates: previousCache.rowCountPropBeforePartialUpdates
  };
};
export function calculatePinnedRowsHeight(apiRef) {
  const pinnedRows = gridPinnedRowsSelector(apiRef);
  const topPinnedRowsHeight = pinnedRows?.top?.reduce((acc, value) => {
    acc += apiRef.current.unstable_getRowHeight(value.id);
    return acc;
  }, 0) || 0;
  const bottomPinnedRowsHeight = pinnedRows?.bottom?.reduce((acc, value) => {
    acc += apiRef.current.unstable_getRowHeight(value.id);
    return acc;
  }, 0) || 0;
  return {
    top: topPinnedRowsHeight,
    bottom: bottomPinnedRowsHeight
  };
}
export function getMinimalContentHeight(apiRef, rowHeight) {
  const densityFactor = gridDensityFactorSelector(apiRef);
  return 2 * Math.floor(rowHeight * densityFactor);
}