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

// This module converts Firefox specific types to the generic types

import {
  hasSource,
  hasSourceActor,
  getSourceActor,
  getSourceCount,
} from "../../selectors/index";
import { features } from "../../utils/prefs";
import {
  isUrlExtension,
  getRawSourceURL,
  getFormattedSourceId,
} from "../../utils/source";
import { createLocation } from "../../utils/location";
import { getDisplayURL } from "../../utils/sources-tree/getURL";

const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");

let store;

/**
 * This function is to be called first before any other
 * and allow having access to any instances of classes that are
 * useful for this module
 *
 * @param {object} dependencies
 * @param {object} dependencies.store
 *                 The redux store object of the debugger frontend.
 */
export function setupCreate(dependencies) {
  store = dependencies.store;
}

export async function createFrame(thread, frame, index = 0) {
  // Because of throttling, the source related to the top frame may be available a bit late.
  const sourceActor = await waitForSourceActorToBeRegisteredInStore(
    frame.where.actor
  );

  const location = createLocation({
    source: sourceActor.sourceObject,
    sourceActor,
    line: frame.where.line,
    column: frame.where.column,
  });

  return {
    id: frame.actorID,
    thread,
    displayName: frame.displayName,
    location,
    generatedLocation: location,
    this: frame.this,
    index,
    asyncCause: frame.asyncCause,
    state: frame.state,
    type: frame.type,
  };
}

/**
 * Create an original frame object. Similar to the frame objects generated by createFrame.
 * But with additional information related to SourceMap processing.
 * For example, location and generatedLocation will be different.
 *
 * @param {Function} getState
 * @param {object} frame
 *        The frame for the generated location, i.e. WASM binary code.
 * @param {string} id
 *        The new ID to use for the new frame object.
 * @param {object} originalFrame
 *        An object crafted by the SourceMap Worker with additional information
 *        about the original source code. i.e. the Rust, C++, whatever original source code.
 *        See XScope.search() for definition of this object.
 */
export function createWasmOriginalFrame(
  generatedFrame,
  id,
  originalFrame,
  originalFrameLocation
) {
  return {
    id,
    thread: generatedFrame.thread,
    displayName: originalFrame.displayName,
    location: originalFrameLocation,
    generatedLocation: generatedFrame.generatedLocation,
    this: generatedFrame.this,
    index: generatedFrame.index,
    asyncCause: generatedFrame.asyncCause,
    state: generatedFrame.state,
    type: generatedFrame.type,

    // All additional fields only available for WASM original frames
    isOriginal: true,
    originalDisplayName: originalFrame.displayName,
    originalVariables: originalFrame.variables,
  };
}

/**
 * This method wait for the given source actor to be registered in Redux store.
 *
 * @param {string} sourceActorId
 *                 Actor ID of the source to be waiting for.
 */
async function waitForSourceActorToBeRegisteredInStore(sourceActorId) {
  if (!hasSourceActor(store.getState(), sourceActorId)) {
    await new Promise(resolve => {
      const unsubscribe = store.subscribe(check);
      let currentSize = null;
      function check() {
        const previousSize = currentSize;
        currentSize = store.getState().sourceActors.mutableSourceActors.size;
        // For perf reason, avoid any extra computation if sources did not change
        if (previousSize == currentSize) {
          return;
        }
        if (hasSourceActor(store.getState(), sourceActorId)) {
          unsubscribe();
          resolve();
        }
      }
    });
  }
  return getSourceActor(store.getState(), sourceActorId);
}

/**
 * This method wait for the given source to be registered in Redux store.
 *
 * @param {string} sourceId
 *                 The id of the source to be waiting for.
 */
export async function waitForSourceToBeRegisteredInStore(sourceId) {
  return new Promise(resolve => {
    if (hasSource(store.getState(), sourceId)) {
      resolve();
      return;
    }
    const unsubscribe = store.subscribe(check);
    let currentSize = null;
    function check() {
      const previousSize = currentSize;
      currentSize = getSourceCount(store.getState());
      // For perf reason, avoid any extra computation if sources did not change
      if (previousSize == currentSize) {
        return;
      }
      if (hasSource(store.getState(), sourceId)) {
        unsubscribe();
        resolve();
      }
    }
  });
}

// Compute the reducer's source ID for a given source front/resource.
//
// We have four kind of "sources":
//   * "sources" in sources.js reducer, which map to 1 or many:
//   * "source actors" in source-actors.js reducer, which map 1 for 1 with:
//   * "SOURCE" resources coming from ResourceCommand API
//   * SourceFront, which are retrieved via `ThreadFront.source(sourceResource)`
//
// Note that SOURCE resources are actually the "form" of the SourceActor,
// with the addition of `resourceType` and `targetFront` attributes.
//
// Unfortunately, the debugger frontend interacts with these 4 type of objects.
// The last three actually try to represent the exact same thing.
//
// Here this method received a SOURCE resource (the 3rd bullet point)
export function makeScriptSourceId(sourceResource) {
  // Allows Jest to use custom, simplier IDs
  if ("mockedJestID" in sourceResource) {
    return sourceResource.mockedJestID;
  }
  // By default, within a given target, all sources will be grouped by URL.
  // You will be having a unique Source object in sources.js reducer,
  // while you might have many Source Actor objects in source-actors.js reducer.
  //
  // There is two distinct usecases here:
  // * HTML pages, which will have one source object which represents the whole HTML page
  //   and it will relate to many source actors. One for each inline <script> tag.
  //   Each script tag's source actor will actually return the whole content of the html page
  //   and not only this one script tag content.
  // * Scripts with the same URL injected many times.
  //   For example, two <script src=""> with the same location
  //   Or by using eval("...// # SourceURL=")
  //   All the scripts will be grouped under a unique Source object, while having dedicated
  //   Source Actor objects.
  //   An important point this time is that each actor may have a different source text content.
  //   For now, the debugger arbitrarily picks the first source actor's text content and never
  //   updates it. (See bug 1751063)
  if (sourceResource.url) {
    return `source-url-${sourceResource.url}`;
  }

  // Otherwise, we are processing a source without URL.
  // This is typically evals, console evaluations, setTimeout/setInterval strings,
  // DOM event handler strings (i.e. `<div onclick="foo">`), ...
  // The main way to interact with them is to use a debugger statement from them,
  // or have other panels ask the debugger to open them (like DOM event handlers from the inspector).
  // We can register transient breakpoints against them (i.e. they will only apply to the current source actor instance)
  return `source-actor-${sourceResource.actor}`;
}

export function makeStyleSheetSourceId(sourceResource) {
  // Base the source id on the actor as we can have multiple stylesheets with the same href
  // (e.g. link to same external style sheet within the HTML page)
  return `source-actor-${sourceResource.resourceId}`;
}

/**
 * Create the source object for a generated source that is stored in the sources.js reducer.
 * These generated sources relate to JS code which run in the
 * debugged runtime (as oppose to original sources
 * which are only available in debugger's environment).
 *
 * @param {SOURCE} sourceResource
 *        SOURCE resource coming from the ResourceCommand API.
 *        This represents the `SourceActor` from the server codebase.
 */
export function createGeneratedSource(sourceResource) {
  return {
    ...createSourceObject({
      id: makeScriptSourceId(sourceResource),
      url: sourceResource.url,
      extensionName: sourceResource.extensionName,
      isExtension:
        (sourceResource.url && isUrlExtension(sourceResource.url)) || false,
    }),
    // Script specific properties
    isWasm: !!features.wasm && sourceResource.introductionType === "wasm",
    isHTML: !!sourceResource.isInlineSource,
    type: ResourceCommand.TYPES.SOURCE,
  };
}

/**
 * Create the source object for a stylesheet source that is stored in the sources.js reducer.
 * These sources relate to CSS
 *
 * @param {*} stylesheetResource
 * @returns
 */
export function createStyleSheet(stylesheetResource) {
  return {
    ...createSourceObject({
      id: makeStyleSheetSourceId(stylesheetResource),
      url: stylesheetResource.href,
    }),
    // Stylesheet specific properties
    isStyleSheet: true,
    type: ResourceCommand.TYPES.STYLESHEET,
  };
}

/**
 * Create the source object that is stored in sources.js reducer.
 *
 * This is an internal helper to this module to ensure all sources have the same shape.
 * Do not use it outside of this module!
 */
function createSourceObject({
  id,
  url,
  extensionName = null,
  isWasm = false,
  isExtension = false,
  isPrettyPrinted = false,
  isOriginal = false,
  isHTML = false,
  isStyleSheet = false,
  generatedSource = null,
  type,
}) {
  // Ensure removing the internal :formatted suffix for the display URL object.
  const displayURL = getDisplayURL(
    isPrettyPrinted ? url.replace(/:formatted$/, "") : url,
    extensionName
  );
  return {
    // The ID, computed by:
    // * `makeScriptSourceId` for generated,
    // * `generatedToOriginalId` for both source map and pretty printed original,
    id,

    // Absolute URL for the source. This may be a fake URL for pretty printed sources
    url,

    // A (slightly tweaked) URL object to represent the source URL.
    // The URL object is augmented of a "group" attribute and some other standard attributes
    // are modified from their typical value. See getDisplayURL implementation.
    displayURL,

    // Short label for this source.
    //
    // * For inlined/eval sources without a URL, the name will refer to the internal source ID,
    // * For pretty printed source, we take care to ignore the internal ":formatted" suffix used in the URL,
    // * For index files, i.e. sources loaded without a filename, they will be named "(index)".
    // * Special characters are decoded from the URL string.
    // (most of this is done by getDisplayURL)
    shortName: url
      ? getRawSourceURL(displayURL.filename)
      : getFormattedSourceId(id),

    // Same as short name, but with the query parameters.
    longName: url
      ? getRawSourceURL(displayURL.filename + displayURL.search)
      : getFormattedSourceId(id),

    // Only set for generated sources that are WebExtension sources.
    // This is especially useful to display the extension name for content scripts
    // that executes against the page we are debugging.
    extensionName,

    // Will be true if the source URL starts with moz-extension://,
    // which most likely means the source is a content script.
    // (Note that when debugging an add-on all generated sources will most likely have this flag set to true)
    isExtension,

    // True if WASM is enabled *and* the generated source is a WASM source (This is only used for script sources)
    isWasm,

    // True if this source is an HTML and relates to many sources actors,
    // one for each of its inline <script> (This is only used for script sources)
    isHTML,

    // True, if this is an original pretty printed source
    isPrettyPrinted,

    // True for source map original files, as well as pretty printed sources
    isOriginal,

    // True for only style sheet sources,
    isStyleSheet,

    // If this is an original/pretty printed source, reference to the related generated/minimized source
    generatedSource,

    // This property defines the type of source object
    type,
  };
}

/**
 * Create the source object for a source mapped original (script/stylesheet) source that is stored in sources.js reducer.
 * These original sources referred to by source maps.
 * This isn't code that runs in the runtime, so it isn't associated with anything
 * on the server side. It is associated with a generated source for the related bundle file
 * which itself relates to an actual code that runs in the runtime.
 *
 * @param {string} id
 *        The ID of the source, computed by source map codebase.
 * @param {string} url
 *        The URL of the original source file.
 * @param {object} generatedSource
 *        The Source object for the related generated source this original source maps to.
 */
export function createSourceMapOriginalSource(id, url, generatedSource) {
  const type = generatedSource.isStyleSheet
    ? ResourceCommand.TYPES.STYLESHEET
    : ResourceCommand.TYPES.SOURCE;
  return createSourceObject({
    id,
    url,
    isOriginal: true,
    generatedSource,
    isStyleSheet: generatedSource.isStyleSheet,
    type,
  });
}

/**
 * Create the source object for a pretty printed original script source that is stored in sources.js reducer.
 * These original pretty printed sources aren't code that run in the runtime,
 * so it isn't associated with anything on the server side.
 * It is associated with a generated source for the non-pretty-printed file
 * which itself relates to an actual code that runs in the runtime.
 *
 * @param {string} id
 *        The ID of the script source, computed by pretty print.
 * @param {string} url
 *        The URL of the pretty-printed source file.
 *        This URL doesn't work. It is the URL of the non-pretty-printed file with ":formated" suffix.
 * @param {object} generatedSource
 *        The Source object for the related minimized source that related to this pretty printed source.
 */
export function createPrettyPrintOriginalScriptSource(
  id,
  url,
  generatedSource
) {
  return {
    ...createSourceObject({
      id,
      url,
      isOriginal: true,
      isPrettyPrinted: true,
      generatedSource,
    }),
    type: ResourceCommand.TYPES.SOURCE,
  };
}

/**
 * Create the source object for a pretty printed original stylesheet source that is stored in sources.js reducer.
 * See `createPrettyPrintOriginalScriptSource`
 *
 * @param {string} id
 *        The ID of the stylesheet source, computed by pretty print.
 * @param {string} url
 *        The URL of the pretty-printed source file.
 *        This URL doesn't work. It is the URL of the non-pretty-printed file with ":formated" suffix.
 * @param {object} generatedSource
 *        The Source object for the related minimized source that related to this pretty printed source.
 */
export function createPrettyPrintOriginalStyleSheetSource(
  id,
  url,
  generatedSource
) {
  return {
    ...createSourceObject({
      id,
      url,
      isOriginal: true,
      isPrettyPrinted: true,
      generatedSource,
    }),
    isStyleSheet: generatedSource.isStyleSheet,
    type: ResourceCommand.TYPES.STYLESHEET,
  };
}

/**
 * Create the "source actor" object that is stored in source-actor.js reducer.
 * This will represent server's source actor in the reducer universe.
 *
 * @param {SOURCE} sourceResource
 *        SOURCE resource coming from the ResourceCommand API.
 *        This represents the `SourceActor` from the server codebase.
 * @param {object} sourceObject
 *        Source object stored in redux, i.e. created via createSourceObject.
 */
export function createScriptSourceActor(sourceResource, sourceObject) {
  const actorId = sourceResource.actor;

  return {
    id: actorId,
    actor: actorId,
    // As sourceResource is only SourceActor's form and not the SourceFront,
    // we have to go through the target to retrieve the related ThreadActor's ID.
    // TODO: Remove this when we move to depend on targets. See Bug 2041750
    thread: sourceResource.targetFront.getCachedFront("thread").actorID,
    targetFront: sourceResource.targetFront,
    sourceObject,
    sourceMapBaseURL: sourceResource.sourceMapBaseURL,
    sourceMapURL: sourceResource.sourceMapURL,
    introductionType: sourceResource.introductionType,
    sourceStartLine: sourceResource.sourceStartLine,
    sourceStartColumn: sourceResource.sourceStartColumn,
    sourceLength: sourceResource.sourceLength,
  };
}

/**
 * Creates a source actor object for a syle sheet that is stored in source-actor.js reducer.
 * This will represent server's source actor in the reducer universe.
 *
 * @param {STYLESHEET} stylesheetResource
 *        STYLESHEET resource coming from the ResourceCommand API.
 *        This represents the `StyleSheetsActor` from the server codebase.
 * @param {object} styleSheetObject
 *        style sheet object stored in redux, i.e. created via createSourceObject.
 */
export function createStyleSheetActor(stylesheetResource, styleSheetObject) {
  return {
    id: stylesheetResource.resourceId,
    actor: stylesheetResource.resourceId,
    targetFront: stylesheetResource.targetFront,
    // TODO: Remove this when we move to depend on targets. See Bug 2041750
    thread: stylesheetResource.targetFront.getCachedFront("thread").actorID,
    // `source` is the reducer source ID
    source: makeStyleSheetSourceId(stylesheetResource),
    sourceObject: styleSheetObject,
    sourceMapBaseURL: stylesheetResource.sourceMapBaseURL,
    sourceMapURL: stylesheetResource.sourceMapURL,
    // There is no `url` for stylesheets, lets use the href
    url: stylesheetResource.href,
  };
}

/**
 * Create a pause info object passed to paused action from
 * the THREAD_STATE "paused" resource.
 */
export async function createPause(threadActorID, pausedThreadState) {
  const frame = await createFrame(threadActorID, pausedThreadState.frame);
  return {
    thread: threadActorID,
    frame,
    why: pausedThreadState.why,
  };
}

export function createThread(targetFront) {
  // When debugging a Web Extension, the top level target is always the fallback document.
  // It isn't really a top level document as it won't be the parent of any other.
  // So only print its name.
  const name =
    targetFront.isTopLevel &&
    !targetFront.commands.descriptorFront.isWebExtension
      ? L10N.getStr("mainThread")
      : targetFront.name;

  return {
    actor: targetFront.targetForm.threadActor,
    url: targetFront.url,
    isTopLevel: targetFront.isTopLevel,
    targetType: targetFront.targetType,
    name,
    serviceWorkerStatus: targetFront.debuggerServiceWorkerStatus,
    processID: targetFront.processID,
    innerWindowId: targetFront.innerWindowId,
  };
}

/**
 * Defines the shape of a breakpoint
 */
export function createBreakpoint({
  id,
  thread,
  disabled = false,
  options = {},
  location,
  generatedLocation,
  text,
  originalText,
}) {
  return {
    // The unique identifier (string) for the breakpoint, for details on its format and creation See `makeBreakpointId`
    id,

    // The thread actor id (string) which the source this breakpoint is created in belongs to
    thread,

    // This (boolean) specifies if the breakpoint is disabled or not
    disabled,

    // This (object) stores extra information about the breakpoint, which defines the type of the breakpoint (i.e conditional breakpoints, log points)
    // {
    //    condition: <Boolean>,
    //    logValue: <String>,
    //    hidden: <Boolean>
    // }
    options,

    // The location (object) information for the original source, for details on its format and structure See `createLocation`
    location,

    // The location (object) information for the generated source, for details on its format and structure See `createLocation`
    generatedLocation,

    // The text (string) on the line which the brekpoint is set in the generated source
    text,

    // The text (string) on the line which the breakpoint is set in the original source
    originalText,
  };
}
