"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.initialSourcesState = initialSourcesState;
exports.default = exports.NO_LOCATION = exports.UNDEFINED_LOCATION = void 0;
loader.lazyRequireGetter(this, "_prefs", "devtools/client/debugger/src/utils/prefs");
loader.lazyRequireGetter(this, "_location", "devtools/client/debugger/src/utils/location");

/* 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/>. */

/**
 * Sources reducer
 * @module reducers/sources
 */
const UNDEFINED_LOCATION = Symbol("Undefined location");
exports.UNDEFINED_LOCATION = UNDEFINED_LOCATION;
const NO_LOCATION = Symbol("No location");
exports.NO_LOCATION = NO_LOCATION;

function initialSourcesState() {
  /* eslint sort-keys: "error" */
  return {
    /**
     * List of all breakpoint positions for all sources (generated and original).
     * Map of source id (string) to dictionary object whose keys are line numbers
     * and values of array of positions.
     * A position is an object made with two attributes:
     * location and generatedLocation. Both refering to breakpoint positions
     * in original and generated sources.
     * In case of generated source, the two location will be the same.
     *
     * Map(source id => Dictionary(int => array<Position>))
     */
    mutableBreakpointPositions: new Map(),

    /**
     * List of all breakable lines for original sources only.
     *
     * Map(source id => promise or array<int> : breakable line numbers>)
     *
     * The value can be a promise to indicate the lines are being loaded.
     */
    mutableOriginalBreakableLines: new Map(),

    /**
     * Map of the source id's to one or more related original source id's
     * Only generated sources which have related original sources will be maintained here.
     *
     * Map(source id => array<Original Source ID>)
     */
    mutableOriginalSources: new Map(),

    /**
     * Mapping of source id's to one or more source-actor's.
     * Dictionary whose keys are source id's and values are arrays
     * made of all the related source-actor's.
     * Note: The source mapped here are only generated sources.
     *
     * "source" are the objects stored in this reducer, in the `sources` attribute.
     * "source-actor" are the objects stored in the "source-actors.js" reducer, in its `sourceActors` attribute.
     *
     * Map(source id => array<Source Actor object>)
     */
    mutableSourceActors: new Map(),

    /**
     * All currently available sources.
     *
     * See create.js: `createSourceObject` method for the description of stored objects.
     */
    mutableSources: new Map(),

    /**
     * All sources associated with a given URL. When using source maps, multiple
     * sources can have the same URL.
     *
     * Map(url => array<source>)
     */
    mutableSourcesPerUrl: new Map(),

    /**
     * When we want to select a source that isn't available yet, use this.
     * The location object should have a url attribute instead of a sourceId.
     *
     * See `createPendingSelectedLocation` for the definition of this object.
     */
    pendingSelectedLocation: _prefs.prefs.pendingSelectedLocation,

    /**
     * The actual currently selected location.
     * Only set if the related source is already registered in the sources reducer.
     * Otherwise, pendingSelectedLocation should be used. Typically for sources
     * which are about to be created.
     *
     * It also includes line and column information.
     *
     * See `createLocation` for the definition of this object.
     */
    selectedLocation: undefined,

    /**
     * When selectedLocation refers to a generated source mapping to an original source
     * via a source-map, refers to the related original location.
     *
     * This is UNDEFINED_LOCATION by default and will switch to NO_LOCATION asynchronously after location
     * selection if there is no valid original location to map to.
     */
    selectedOriginalLocation: UNDEFINED_LOCATION,

    /**
     * By default, the `selectedLocation` should be highlighted in the editor with a special background.
     * On demand, this flag can be set to false in order to prevent this.
     * The location will be shown, but not highlighted.
     */
    shouldHighlightSelectedLocation: true,

    /**
     * By default, if we have a source-mapped source, we would automatically try
     * to select and show the content of the original source. But, if we explicitly
     * select a generated source, we remember this choice. That, until we explicitly
     * select an original source.
     * Note that selections related to non-source-mapped sources should never
     * change this setting.
     */
    shouldSelectOriginalLocation: true
  };
  /* eslint-disable sort-keys */
}

function update(state = initialSourcesState(), action) {
  switch (action.type) {
    case "ADD_SOURCES":
      return addSources(state, action.sources);

    case "ADD_ORIGINAL_SOURCES":
      return addSources(state, action.originalSources);

    case "INSERT_SOURCE_ACTORS":
      return insertSourceActors(state, action);

    case "SET_SELECTED_LOCATION":
      {
        let pendingSelectedLocation = null;

        if (action.location.source.url) {
          pendingSelectedLocation = (0, _location.createPendingSelectedLocation)(action.location);
          _prefs.prefs.pendingSelectedLocation = pendingSelectedLocation;
        }

        return { ...state,
          selectedLocation: action.location,
          selectedOriginalLocation: UNDEFINED_LOCATION,
          pendingSelectedLocation,
          shouldSelectOriginalLocation: action.shouldSelectOriginalLocation,
          shouldHighlightSelectedLocation: action.shouldHighlightSelectedLocation,
          shouldScrollToSelectedLocation: action.shouldScrollToSelectedLocation
        };
      }

    case "CLEAR_SELECTED_LOCATION":
      {
        const pendingSelectedLocation = {
          url: ""
        };
        _prefs.prefs.pendingSelectedLocation = pendingSelectedLocation;
        return { ...state,
          selectedLocation: null,
          selectedOriginalLocation: UNDEFINED_LOCATION,
          pendingSelectedLocation
        };
      }

    case "SET_ORIGINAL_SELECTED_LOCATION":
      {
        if (action.location != state.selectedLocation) {
          return state;
        }

        return { ...state,
          selectedOriginalLocation: action.originalLocation
        };
      }

    case "SET_GENERATED_SELECTED_LOCATION":
      {
        if (action.location != state.selectedLocation) {
          return state;
        }

        return { ...state,
          selectedGeneratedLocation: action.generatedLocation
        };
      }

    case "SET_DEFAULT_SELECTED_LOCATION":
      {
        if (state.shouldSelectOriginalLocation == action.shouldSelectOriginalLocation) {
          return state;
        }

        return { ...state,
          shouldSelectOriginalLocation: action.shouldSelectOriginalLocation
        };
      }

    case "SET_PENDING_SELECTED_LOCATION":
      {
        const pendingSelectedLocation = {
          url: action.url,
          line: action.line,
          column: action.column
        };
        _prefs.prefs.pendingSelectedLocation = pendingSelectedLocation;
        return { ...state,
          pendingSelectedLocation
        };
      }

    case "SET_ORIGINAL_BREAKABLE_LINES":
      {
        state.mutableOriginalBreakableLines.set(action.source.id, action.promise || action.breakableLines);
        return { ...state
        };
      }

    case "ADD_BREAKPOINT_POSITIONS":
      {
        // Merge existing and new reported position if some where already stored
        let positions = state.mutableBreakpointPositions.get(action.source.id);

        if (positions) {
          positions = { ...positions,
            ...action.positions
          };
        } else {
          positions = action.positions;
        }

        state.mutableBreakpointPositions.set(action.source.id, positions);
        return { ...state
        };
      }

    case "CLEAR_BREAKPOINT_POSITIONS":
      {
        if (!state.mutableBreakpointPositions.has(action.source.id)) {
          return state;
        }

        state.mutableBreakpointPositions.delete(action.source.id);
        return { ...state
        };
      }

    case "REMOVE_SOURCES":
      {
        return removeSourcesAndActors(state, action);
      }
  }

  return state;
}
/*
 * Add sources to the sources store
 * - Add the source to the sources store
 * - Add the source URL to the source url map
 */


function addSources(state, sources) {
  for (const source of sources) {
    state.mutableSources.set(source.id, source); // Update the source url map

    const existing = state.mutableSourcesPerUrl.get(source.url);

    if (existing) {
      // We never return this array from selectors as-is,
      // we either return the first entry or lookup for a precise entry
      // so we can mutate it.
      existing.push(source);
    } else {
      state.mutableSourcesPerUrl.set(source.url, [source]);
    } // In case of original source, maintain the mapping of generated source to original sources map.


    if (source.isOriginal) {
      const generatedSourceId = source.generatedSource.id;
      let originalSourceIds = state.mutableOriginalSources.get(generatedSourceId);

      if (!originalSourceIds) {
        originalSourceIds = [];
        state.mutableOriginalSources.set(generatedSourceId, originalSourceIds);
      } // We never return this array out of selectors, so mutate the list


      originalSourceIds.push(source.id);
    }
  }

  return { ...state
  };
}

function removeSourcesAndActors(state, action) {
  const {
    mutableSourcesPerUrl,
    mutableSources,
    mutableOriginalSources,
    mutableSourceActors,
    mutableOriginalBreakableLines,
    mutableBreakpointPositions
  } = state;
  const newState = { ...state
  };

  for (const removedSource of action.sources) {
    const sourceId = removedSource.id; // Clear the urls Map

    const sourceUrl = removedSource.url;

    if (sourceUrl) {
      const sourcesForSameUrl = (mutableSourcesPerUrl.get(sourceUrl) || []).filter(s => s != removedSource);

      if (!sourcesForSameUrl.length) {
        // All sources with this URL have been removed
        mutableSourcesPerUrl.delete(sourceUrl);
      } else {
        // There are other sources still alive with the same URL
        mutableSourcesPerUrl.set(sourceUrl, sourcesForSameUrl);
      }
    }

    mutableSources.delete(sourceId); // Note that the caller of this method queried the reducer state
    // to aggregate the related original sources.
    // So if we were having related original sources, they will be
    // in `action.sources`.

    mutableOriginalSources.delete(sourceId); // If a source is removed, immediately remove all its related source actors.
    // It can speed-up the following for loop cleaning actors.

    mutableSourceActors.delete(sourceId);

    if (removedSource.isOriginal) {
      mutableOriginalBreakableLines.delete(sourceId); // Also ensure removing this original source id in the array specific to its
      // generated source

      const generatedSourceId = removedSource.generatedSource.id;
      let originalSourceIds = mutableOriginalSources.get(generatedSourceId);

      if (originalSourceIds) {
        originalSourceIds = originalSourceIds.filter(id => id != sourceId);
        mutableOriginalSources.set(generatedSourceId, originalSourceIds);
      } // We should also remove the mapped location from the breakpoint positions
      //
      // `mutableBreakpointPositions` is a Map keyed per generated source id
      //   `generatedBreakpointPositions` is a Array
      //     `position` is an object with `location` and `generatedLocation` attributes


      const generatedBreakpointPositions = mutableBreakpointPositions.get(generatedSourceId);

      if (generatedBreakpointPositions) {
        for (const line in generatedBreakpointPositions) {
          for (const position of generatedBreakpointPositions[line]) {
            // Only clear the original mapped location if that's a breakpoint
            // for the currently removed original source. This generated/bundle source
            // may have breakpoints for many original sources.
            if (position.location.source == removedSource) {
              position.location = position.generatedLocation;
            }
          }
        }
      }
    }

    mutableBreakpointPositions.delete(sourceId);

    if (action.resetSelectedLocation && newState.selectedLocation?.source == removedSource) {
      newState.selectedLocation = null;
      newState.selectedOriginalLocation = UNDEFINED_LOCATION;
    }
  }

  for (const removedActor of action.actors) {
    const sourceId = removedActor.source;
    const actorsForSource = mutableSourceActors.get(sourceId); // actors may have already been cleared by the previous for..loop

    if (!actorsForSource) {
      continue;
    }

    const idx = actorsForSource.indexOf(removedActor);

    if (idx != -1) {
      actorsForSource.splice(idx, 1); // While the Map is mutable, we expect new array instance on each new change

      mutableSourceActors.set(sourceId, [...actorsForSource]);
    } // Remove the entry in the Map if there is no more actors for that source


    if (!actorsForSource.length) {
      mutableSourceActors.delete(sourceId);
    }

    if (action.resetSelectedLocation && newState.selectedLocation?.sourceActor == removedActor) {
      newState.selectedLocation = null;
      newState.selectedOriginalLocation = UNDEFINED_LOCATION;
    }
  }

  return newState;
}

function insertSourceActors(state, action) {
  const {
    sourceActors
  } = action;
  const {
    mutableSourceActors
  } = state; // The `sourceActor` objects are defined from `newGeneratedSources` action:
  // https://searchfox.org/mozilla-central/rev/4646b826a25d3825cf209db890862b45fa09ffc3/devtools/client/debugger/src/actions/sources/newSources.js#300-314

  for (const sourceActor of sourceActors) {
    const sourceId = sourceActor.source; // We always clone the array of source actors as we return it from selectors.
    // So the map is mutable, but its values are considered immutable and will change
    // anytime there is a new actor added per source ID.

    const existing = mutableSourceActors.get(sourceId);

    if (existing) {
      mutableSourceActors.set(sourceId, [...existing, sourceActor]);
    } else {
      mutableSourceActors.set(sourceId, [sourceActor]);
    }
  }

  const scriptActors = sourceActors.filter(item => item.introductionType === "scriptElement");

  if (scriptActors.length) {
    // If new HTML sources are being added, we need to clear the breakpoint
    // positions since the new source is a <script> with new breakpoints.
    for (const {
      source
    } of scriptActors) {
      state.mutableBreakpointPositions.delete(source);
    }
  }

  return { ...state
  };
}

var _default = update;
exports.default = _default;