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

/**
 * Redux actions for the search state
 *
 * @module actions/search
 */

import { isFulfilled } from "../utils/async-value";
import {
  getFirstSourceActorForGeneratedSource,
  getSourceList,
  getSettledSourceTextContent,
  isSourceBlackBoxed,
  getSearchOptions,
} from "../selectors/index";
import { createLocation } from "../utils/location";
import { matchesGlobPatterns } from "../utils/source";
import { loadSourceText } from "./sources/loadSourceText";
import { searchKeys } from "../constants";

export function searchSources(query, onUpdatedResults, signal) {
  return async ({ dispatch, getState, searchWorker }) => {
    dispatch({
      type: "SET_PROJECT_SEARCH_QUERY",
      query,
    });

    const searchOptions = getSearchOptions(
      getState(),
      searchKeys.PROJECT_SEARCH
    );
    const validSources = getSourceList(getState()).filter(
      source =>
        !isSourceBlackBoxed(getState(), source) &&
        !matchesGlobPatterns(source, searchOptions.excludePatterns)
    );
    // Sort original entries first so that search results are more useful.
    // Deprioritize third-party scripts, so their results show last.
    validSources.sort((a, b) => {
      function isThirdParty(source) {
        return (
          source?.url &&
          (source.url.includes("node_modules") ||
            source.url.includes("bower_components"))
        );
      }

      if (a.isOriginal && !isThirdParty(a)) {
        return -1;
      }

      if (b.isOriginal && !isThirdParty(b)) {
        return 1;
      }

      if (!isThirdParty(a) && isThirdParty(b)) {
        return -1;
      }
      if (isThirdParty(a) && !isThirdParty(b)) {
        return 1;
      }
      return 0;
    });
    const results = [];
    for (const source of validSources) {
      const sourceActor = getFirstSourceActorForGeneratedSource(
        getState(),
        source.id
      );
      await dispatch(loadSourceText(source, sourceActor));

      // This is the only asynchronous call in this method.
      // We may have stopped the search by closing the search panel or changing the query.
      // Avoid any further unecessary computation when the React Component tells us the query was cancelled.
      if (signal.aborted) {
        return;
      }

      const result = await searchSource(source, sourceActor, query, {
        getState,
        searchWorker,
      });
      if (signal.aborted) {
        return;
      }

      if (result) {
        results.push(result);
        onUpdatedResults(results, false, signal);
      }
    }
    onUpdatedResults(results, true, signal);
  };
}

export async function searchSource(
  source,
  sourceActor,
  query,
  { getState, searchWorker }
) {
  const state = getState();
  const location = createLocation({
    source,
    sourceActor,
  });

  const content = getSettledSourceTextContent(state, location);
  let matches = [];

  if (content && isFulfilled(content) && content.value.type === "text") {
    const options = getSearchOptions(state, searchKeys.PROJECT_SEARCH);
    matches = await searchWorker.findSourceMatches(
      content.value,
      query,
      options
    );
  }
  if (!matches.length) {
    return null;
  }
  return {
    type: "RESULT",
    location,
    // `matches` are generated by project-search worker's `findSourceMatches` method
    matches: matches.map(m => ({
      type: "MATCH",
      location: createLocation({
        ...location,
        // `matches` only contain line and column
        // `location` will already refer to the right source/sourceActor
        line: m.line,
        column: m.column,
      }),
      matchIndex: m.matchIndex,
      match: m.match,
      value: m.value,
    })),
  };
}
