"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.updateBreakpointPositionsForNewPrettyPrintedSource = updateBreakpointPositionsForNewPrettyPrintedSource;
exports.setBreakpointPositions = void 0;
loader.lazyRequireGetter(this, "_index", "devtools/client/debugger/src/selectors/index");
loader.lazyRequireGetter(this, "_index2", "devtools/client/debugger/src/utils/breakpoint/index");
loader.lazyRequireGetter(this, "_memoizableAction", "devtools/client/debugger/src/utils/memoizableAction");
loader.lazyRequireGetter(this, "_asyncValue", "devtools/client/debugger/src/utils/async-value");
loader.lazyRequireGetter(this, "_location", "devtools/client/debugger/src/utils/location");
loader.lazyRequireGetter(this, "_context", "devtools/client/debugger/src/utils/context");

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */

/**
 * Helper function which consumes breakpoints positions sent by the server
 * and map them to location objects.
 * During this process, the SourceMapLoader will be queried to map the positions from generated to original locations.
 *
 * @param {Object} breakpointPositions
 *        The positions to map related to the generated source:
 *          {
 *            1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
 *            2: [ 2 ], // Line 2 is only breakable on column 2
 *          }
 * @param {Object} generatedSource
 * @param {Object} location
 *        The current location we are computing breakable positions.
 * @param {Object} thunk arguments
 * @return {Object}
 *         The mapped breakable locations in the original source:
 *          {
 *            1: [ { source, line: 1, column: 2} , { source, line: 1, column 6 } ], // Line 1 is not mapped as location are same as breakpointPositions.
 *            10: [ { source, line: 10, column: 28 } ], // Line 2 is mapped and locations and line key refers to the original source positions.
 *          }
 */
async function mapToLocations(breakpointPositions, generatedSource, mappedLocation, {
  getState,
  sourceMapLoader
}) {
  // Map breakable positions from generated to original locations.
  let mappedBreakpointPositions = await sourceMapLoader.getOriginalLocations(breakpointPositions, generatedSource.id); // The Source Map Loader will return null when there is no source map for that generated source.
  // Consider the map as unrelated to source map and process the source actor positions as-is.

  if (!mappedBreakpointPositions) {
    mappedBreakpointPositions = breakpointPositions;
  }

  const positions = {}; // Ensure that we have an entry for the line fetched

  if (typeof mappedLocation.line === "number") {
    positions[mappedLocation.line] = [];
  }

  const handledBreakpointIds = new Set();
  const isOriginal = mappedLocation.source.isOriginal;
  const originalSourceId = mappedLocation.source.id;

  for (let line in mappedBreakpointPositions) {
    // createLocation expects a number and not a string.
    line = parseInt(line, 10);

    for (const columnOrSourceMapLocation of mappedBreakpointPositions[line]) {
      let location, generatedLocation; // When processing a source unrelated to source map, `mappedBreakpointPositions` will be equal to `breakpointPositions`.
      // and columnOrSourceMapLocation will always be a number.
      // But it will also be a number if we process a source mapped file and SourceMapLoader didn't find any valid mapping
      // for the current position (line and column).
      // When this happen to be a number it means it isn't mapped and columnOrSourceMapLocation refers to the column index.

      if (typeof columnOrSourceMapLocation == "number") {
        // If columnOrSourceMapLocation is a number, it means that this location doesn't mapped to an original source.
        // So if we are currently computation positions for an original source, we can skip this breakable positions.
        if (isOriginal) {
          continue;
        }

        location = generatedLocation = (0, _location.createLocation)({
          line,
          column: columnOrSourceMapLocation,
          source: generatedSource
        });
      } else {
        // Otherwise, for sources which are mapped. `columnOrSourceMapLocation` will be a SourceMapLoader location object.
        // This location object will refer to the location where the current column (columnOrSourceMapLocation.generatedColumn)
        // mapped in the original file.
        // When computing positions for an original source, ignore the location if that mapped to another original source.
        if (isOriginal && columnOrSourceMapLocation.sourceId != originalSourceId) {
          continue;
        }

        location = (0, _location.sourceMapToDebuggerLocation)(getState(), columnOrSourceMapLocation); // Merge positions that refer to duplicated positions.
        // Some sourcemaped positions might refer to the exact same source/line/column triple.

        const breakpointId = (0, _index2.makeBreakpointId)(location);

        if (handledBreakpointIds.has(breakpointId)) {
          continue;
        }

        handledBreakpointIds.add(breakpointId);
        generatedLocation = (0, _location.createLocation)({
          line,
          column: columnOrSourceMapLocation.generatedColumn,
          source: generatedSource
        });
      } // The positions stored in redux will be keyed by original source's line (if we are
      // computing the original source positions), or the generated source line.
      // Note that when we compute the bundle positions, location may refer to the original source,
      // but we still want to use the generated location as key.


      const keyLocation = isOriginal ? location : generatedLocation;
      const keyLine = keyLocation.line;

      if (!positions[keyLine]) {
        positions[keyLine] = [];
      }

      positions[keyLine].push({
        location,
        generatedLocation
      });
    }
  }

  return positions;
}

async function _setBreakpointPositions(location, thunkArgs) {
  const {
    client,
    dispatch,
    getState,
    sourceMapLoader
  } = thunkArgs;
  const results = {};
  let generatedSource = location.source;

  if (location.source.isOriginal) {
    const ranges = await sourceMapLoader.getGeneratedRangesForOriginal(location.source.id, true);
    generatedSource = location.source.generatedSource; // Note: While looping here may not look ideal, in the vast majority of
    // cases, the number of ranges here should be very small, and is quite
    // likely to only be a single range.

    for (const range of ranges) {
      // Wrap infinite end positions to the next line to keep things simple
      // and because we know we don't care about the end-line whitespace
      // in this case.
      if (range.end.column === Infinity) {
        range.end = {
          line: range.end.line + 1,
          column: 0
        };
      } // Retrieve the positions for all the source actors for the related generated source.
      // There might be many if it is loaded many times.
      // We limit the retrieval of positions within the given range, so that we don't
      // retrieve the whole bundle positions.


      const allActorsPositions = await Promise.all((0, _index.getSourceActorsForSource)(getState(), generatedSource.id).map(actor => client.getSourceActorBreakpointPositions(actor, range))); // `allActorsPositions` looks like this:
      // [
      //   { // Positions for the first source actor
      //     1: [ 2, 6 ], // Line 1 is breakable on column 2 and 6
      //     2: [ 2 ], // Line 2 is only breakable on column 2
      //   },
      //   {...} // Positions for another source actor
      // ]

      for (const actorPositions of allActorsPositions) {
        for (const rangeLine in actorPositions) {
          const columns = actorPositions[rangeLine]; // Merge all actors's breakable columns and avoid duplication of columns reported as breakable

          const existing = results[rangeLine];

          if (existing) {
            for (const column of columns) {
              if (!existing.includes(column)) {
                existing.push(column);
              }
            }
          } else {
            results[rangeLine] = columns;
          }
        }
      }
    }
  } else {
    const {
      line
    } = location;

    if (typeof line !== "number") {
      throw new Error("Line is required for generated sources");
    } // We only retrieve the positions for the given requested line, that, for each source actor.
    // There might be many source actor, if it is loaded many times.
    // Or if this is an html page, with many inline scripts.


    const allActorsBreakableColumns = await Promise.all((0, _index.getSourceActorsForSource)(getState(), location.source.id).map(async actor => {
      const positions = await client.getSourceActorBreakpointPositions(actor, {
        // Only retrieve positions for the given line
        start: {
          line,
          column: 0
        },
        end: {
          line: line + 1,
          column: 0
        }
      });
      return positions[line] || [];
    }));

    for (const columns of allActorsBreakableColumns) {
      // Merge all actors's breakable columns and avoid duplication of columns reported as breakable
      const existing = results[line];

      if (existing) {
        for (const column of columns) {
          if (!existing.includes(column)) {
            existing.push(column);
          }
        }
      } else {
        results[line] = columns;
      }
    }
  }

  const positions = await mapToLocations(results, generatedSource, location, thunkArgs); // `mapToLocations` may compute for a little while asynchronously,
  // ensure that the location is still valid before continuing.

  (0, _context.validateSource)(getState(), location.source);
  dispatch({
    type: "ADD_BREAKPOINT_POSITIONS",
    source: location.source,
    positions
  });
}

function generatedSourceActorKey(state, source) {
  const generatedSource = source.isOriginal ? source.generatedSource : source;
  const actors = generatedSource ? (0, _index.getSourceActorsForSource)(state, generatedSource.id).map(({
    actor
  }) => actor) : [];
  return [source.id, ...actors].join(":");
}
/**
 * This method will force retrieving the breakable positions for a given source, on a given line.
 * If this data has already been computed, it will returned the cached data.
 *
 * For original sources, this will query the SourceMap worker.
 * For generated sources, this will query the DevTools server and the related source actors.
 *
 * @param Object options
 *        Dictionary object with many arguments:
 * @param String options.sourceId
 *        The source we want to fetch breakable positions
 * @param Number options.line
 *        The line we want to know which columns are breakable.
 *        (note that this seems to be optional for original sources)
 * @return Array<Object>
 *         The list of all breakable positions, each object of this array will be like this:
 *         {
 *           line: Number
 *           column: Number
 *           source: Source object
 *         }
 */


const setBreakpointPositions = (0, _memoizableAction.memoizeableAction)("setBreakpointPositions", {
  getValue: (location, {
    getState
  }) => {
    const positions = (0, _index.getBreakpointPositionsForSource)(getState(), location.source.id);

    if (!positions) {
      return null;
    }

    if (!location.source.isOriginal && location.line && !positions[location.line]) {
      // We always return the full position dataset, but if a given line is
      // not available, we treat the whole set as loading.
      return null;
    }

    return (0, _asyncValue.fulfilled)(positions);
  },

  createKey(location, {
    getState
  }) {
    const key = generatedSourceActorKey(getState(), location.source);
    return !location.source.isOriginal && location.line ? `${key}-${location.line}` : key;
  },

  action: async (location, thunkArgs) => _setBreakpointPositions(location, thunkArgs)
});
exports.setBreakpointPositions = setBreakpointPositions;

function updateBreakpointPositionsForNewPrettyPrintedSource(minifiedSource) {
  return async ({
    dispatch,
    getState
  }) => {
    const oldPositions = (0, _index.getBreakpointPositionsForSource)(getState(), minifiedSource.id);

    if (!oldPositions) {
      return;
    } // gather the lines for which we have breakpointPositions


    const lines = [...Object.keys(oldPositions)].map(lineString => Number(lineString));
    dispatch({
      type: "CLEAR_BREAKPOINT_POSITIONS",
      source: minifiedSource
    }); // recompute the breakpoint positions for all lines for which we had breakpointPositions before

    await Promise.all(lines.map(line => dispatch(setBreakpointPositions({
      source: minifiedSource,
      line
    }))));
  };
}