const { fetch, assert } = require("devtools/shared/DevToolsUtils");
const { joinURI } = require("devtools/shared/path");
const path = require("sdk/fs/path");
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
const { isJavaScript } = require("./source");
const {
  originalToGeneratedId,
  generatedToOriginalId,
  isGeneratedId,
  isOriginalId
} = require("./source-map-util");

let sourceMapRequests = new Map();
let sourceMapsEnabled = false;

function clearSourceMaps() {
  sourceMapRequests.clear();
}

function enableSourceMaps() {
  sourceMapsEnabled = true;
}

function _resolveSourceMapURL(source) {
  const { url = "", sourceMapURL = "" } = source;
  
  const UNSUPPORTED_PROTOCOLS = ["chrome://", "resource://"];
  if (path.isURL(sourceMapURL) && UNSUPPORTED_PROTOCOLS.some(protocol => sourceMapURL.startsWith(protocol))) {
    // If it's an internal protocol, don't allow it and return empty.
    return "";
  }
  if (path.isURL(sourceMapURL) && sourceMapURL.startsWith("file://")) {
    // Only allow file:// source maps from file:// docs
    if (!url.startsWith("file://")) {
      return "";
    }
  }
  
  if (path.isURL(sourceMapURL) || url == "") {
    // If it's already a full URL or the source doesn't have a URL,
    // don't resolve anything.
    return sourceMapURL;
  } else if (path.isAbsolute(sourceMapURL)) {
    // If it's an absolute path, it should be resolved relative to the
    // host of the source.
    const { protocol = "", host = "" } = parse(url);
    return `${protocol}//${host}${sourceMapURL}`;
  }
  // Otherwise, it's a relative path and should be resolved relative
  // to the source.
  return dirname(url) + "/" + sourceMapURL;
}

/**
 * Sets the source map's sourceRoot to be relative to the source map url.
 * @memberof utils/source-map-worker
 * @static
 */
function _setSourceMapRoot(sourceMap, absSourceMapURL, source) {
  // No need to do this fiddling if we won't be fetching any sources over the
  // wire.
  if (sourceMap.hasContentsOfAllSources()) {
    return;
  }

  const base = dirname(
    (absSourceMapURL.indexOf("data:") === 0 && source.url) ?
      source.url :
      absSourceMapURL
  );

  if (sourceMap.sourceRoot) {
    sourceMap.sourceRoot = joinURI(base, sourceMap.sourceRoot);
  } else {
    sourceMap.sourceRoot = base;
  }

  return sourceMap;
}

function _getSourceMap(generatedSourceId)
    : ?Promise<SourceMapConsumer> {
  return sourceMapRequests.get(generatedSourceId);
}

async function _resolveAndFetch(generatedSource) : SourceMapConsumer {
  // Fetch the sourcemap over the network and create it.
  const sourceMapURL = _resolveSourceMapURL(generatedSource);
  const fetched = await fetch(
    sourceMapURL, { loadFromCache: false,
                    redirect: error }
  );

  // Create the source map and fix it up.
  const map = new SourceMapConsumer(fetched.content);
  _setSourceMapRoot(map, sourceMapURL, generatedSource);
  return map;
}

function _fetchSourceMap(generatedSource) {
  const existingRequest = sourceMapRequests.get(generatedSource.id);
  if (existingRequest) {
    // If it has already been requested, return the request. Make sure
    // to do this even if sourcemapping is turned off, because
    // pretty-printing uses sourcemaps.
    //
    // An important behavior here is that if it's in the middle of
    // requesting it, all subsequent calls will block on the initial
    // request.
    return existingRequest;
  } else if (!generatedSource.sourceMapURL || !sourceMapsEnabled) {
    return Promise.resolve(null);
  }

  // Fire off the request, set it in the cache, and return it.
  // Suppress any errors and just return null (ignores bogus
  // sourcemaps).
  const req = _resolveAndFetch(generatedSource).catch(() => null);
  sourceMapRequests.set(generatedSource.id, req);
  return req;
}

async function getOriginalURLs(generatedSource) {
  const map = await _fetchSourceMap(generatedSource);
  return map && map.sources;
}

async function getGeneratedLocation(location: Location, originalSource: Source)
    : Promise<Location> {
  if (!isOriginalId(location.sourceId)) {
    return location;
  }

  const generatedSourceId = originalToGeneratedId(location.sourceId);
  const map = await _getSourceMap(generatedSourceId);
  if (!map) {
    return location;
  }

  const { line, column } = map.generatedPositionFor({
    source: originalSource.url,
    line: location.line,
    column: location.column == null ? 0 : location.column
  });

  return {
    sourceId: generatedSourceId,
    line: line,
    // Treat 0 as no column so that line breakpoints work correctly.
    column: column === 0 ? undefined : column
  };
}

async function getOriginalLocation(location) {
  if (!isGeneratedId(location.sourceId)) {
    return location;
  }

  const map = await _getSourceMap(location.sourceId);
  if (!map) {
    return location;
  }

  const { source: url, line, column } = map.originalPositionFor({
    line: location.line,
    column: location.column == null ? Infinity : location.column
  });

  if (url == null) {
    // No url means the location didn't map.
    return location;
  }

  return {
    sourceId: generatedToOriginalId(location.sourceId, url),
    line,
    column
  };
}

async function getOriginalSourceText(originalSource) {
  assert(isOriginalId(originalSource.id),
         "Source is not an original source");

  const generatedSourceId = originalToGeneratedId(originalSource.id);
  const map = await _getSourceMap(generatedSourceId);
  if (!map) {
    return null;
  }

  let text = map.sourceContentFor(originalSource.url);
  if (!text) {
    text = (await fetch(
      originalSource.url, { loadFromCache: false }
    )).content;
  }

  return {
    text,
    contentType: isJavaScript(originalSource.url || "") ?
      "text/javascript" :
      "text/plain"
  };
}

function applySourceMap(generatedId, url, code, mappings) {
  const generator = new SourceMapGenerator({ file: url });
  mappings.forEach(mapping => generator.addMapping(mapping));
  generator.setSourceContent(url, code);

  const map = SourceMapConsumer(generator.toJSON());
  sourceMapRequests.set(generatedId, Promise.resolve(map));
}

const publicInterface = {
  getOriginalURLs,
  getGeneratedLocation,
  getOriginalLocation,
  getOriginalSourceText,
  enableSourceMaps,
  applySourceMap,
  clearSourceMaps
};

self.onmessage = function(msg) {
  const { id, method, args } = msg.data;
  const response = publicInterface[method].apply(undefined, args);
  if (response instanceof Promise) {
    response.then(val => self.postMessage({ id, response: val }),
                  err => self.postMessage({ id, error: err }));
  } else {
    self.postMessage({ id, response });
  }
};
