// Copyright 2014 Evan Wallace
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
// Originated from source-map-support but has been heavily modified for deno.

import { SourceMapConsumer, MappedPosition } from "source-map";
import * as base64 from "base64-js";
import { arrayToStr } from "./util";
import { CallSite, RawSourceMap } from "./types";

const consumers = new Map<string, SourceMapConsumer>();

interface Options {
  // A callback the returns generated file contents.
  getGeneratedContents: GetGeneratedContentsCallback;
  // Usually set the following to true. Set to false for testing.
  installPrepareStackTrace: boolean;
}

interface Position {
  source: string; // Filename
  column: number;
  line: number;
}

type GetGeneratedContentsCallback = (fileName: string) => string | RawSourceMap;

let getGeneratedContents: GetGeneratedContentsCallback;

// @internal
export function install(options: Options) {
  getGeneratedContents = options.getGeneratedContents;
  if (options.installPrepareStackTrace) {
    Error.prepareStackTrace = prepareStackTraceWrapper;
  }
}

// @internal
export function prepareStackTraceWrapper(
  error: Error,
  stack: CallSite[]
): string {
  try {
    return prepareStackTrace(error, stack);
  } catch (prepareStackError) {
    Error.prepareStackTrace = undefined;
    console.log("=====Error inside of prepareStackTrace====");
    console.log(prepareStackError.stack.toString());
    console.log("=====Original error=======================");
    throw error;
  }
}

// @internal
export function prepareStackTrace(error: Error, stack: CallSite[]): string {
  const frames = stack.map(
    (frame: CallSite) => `\n    at ${wrapCallSite(frame).toString()}`
  );
  return error.toString() + frames.join("");
}

// @internal
export function wrapCallSite(frame: CallSite): CallSite {
  if (frame.isNative()) {
    return frame;
  }

  // Most call sites will return the source file from getFileName(), but code
  // passed to eval() ending in "//# sourceURL=..." will return the source file
  // from getScriptNameOrSourceURL() instead
  const source = frame.getFileName() || frame.getScriptNameOrSourceURL();

  if (source) {
    const line = frame.getLineNumber() || 0;
    const column = (frame.getColumnNumber() || 1) - 1;
    const position = mapSourcePosition({ source, line, column });
    frame = cloneCallSite(frame);
    frame.getFileName = () => position.source;
    frame.getLineNumber = () => position.line;
    frame.getColumnNumber = () => Number(position.column) + 1;
    frame.getScriptNameOrSourceURL = () => position.source;
    frame.toString = () => CallSiteToString(frame);
    return frame;
  }

  // Code called using eval() needs special handling
  let origin = (frame.isEval() && frame.getEvalOrigin()) || undefined;
  if (origin) {
    origin = mapEvalOrigin(origin);
    frame = cloneCallSite(frame);
    frame.getEvalOrigin = () => origin;
    frame.toString = () => CallSiteToString(frame);
    return frame;
  }

  // If we get here then we were unable to change the source position
  return frame;
}

function cloneCallSite(frame: CallSite): CallSite {
  // tslint:disable:no-any
  const obj: any = {};
  const frame_ = frame as any;
  const props = Object.getOwnPropertyNames(Object.getPrototypeOf(frame));
  props.forEach(name => {
    obj[name] = /^(?:is|get)/.test(name)
      ? () => frame_[name].call(frame)
      : frame_[name];
  });
  return (obj as any) as CallSite;
  // tslint:enable:no-any
}

// Taken from source-map-support, original copied from V8's messages.js
// MIT License. Copyright (c) 2014 Evan Wallace
function CallSiteToString(frame: CallSite): string {
  let fileName;
  let fileLocation = "";
  if (frame.isNative()) {
    fileLocation = "native";
  } else {
    fileName = frame.getScriptNameOrSourceURL();
    if (!fileName && frame.isEval()) {
      fileLocation = frame.getEvalOrigin() || "";
      fileLocation += ", "; // Expecting source position to follow.
    }

    if (fileName) {
      fileLocation += fileName;
    } else {
      // Source code does not originate from a file and is not native, but we
      // can still get the source position inside the source string, e.g. in
      // an eval string.
      fileLocation += "<anonymous>";
    }
    const lineNumber = frame.getLineNumber();
    if (lineNumber != null) {
      fileLocation += ":" + String(lineNumber);
      const columnNumber = frame.getColumnNumber();
      if (columnNumber) {
        fileLocation += ":" + String(columnNumber);
      }
    }
  }

  let line = "";
  const functionName = frame.getFunctionName();
  let addSuffix = true;
  const isConstructor = frame.isConstructor();
  const isMethodCall = !(frame.isToplevel() || isConstructor);
  if (isMethodCall) {
    let typeName = frame.getTypeName();
    // Fixes shim to be backward compatable with Node v0 to v4
    if (typeName === "[object Object]") {
      typeName = "null";
    }
    const methodName = frame.getMethodName();
    if (functionName) {
      if (typeName && functionName.indexOf(typeName) !== 0) {
        line += typeName + ".";
      }
      line += functionName;
      if (
        methodName &&
        functionName.indexOf("." + methodName) !==
          functionName.length - methodName.length - 1
      ) {
        line += ` [as ${methodName} ]`;
      }
    } else {
      line += `${typeName}.${methodName || "<anonymous>"}`;
    }
  } else if (isConstructor) {
    line += `new ${functionName || "<anonymous>"}`;
  } else if (functionName) {
    line += functionName;
  } else {
    line += fileLocation;
    addSuffix = false;
  }
  if (addSuffix) {
    line += ` (${fileLocation})`;
  }
  return line;
}

// Regex for detecting source maps
const reSourceMap = /^data:application\/json[^,]+base64,/;

function loadConsumer(source: string): SourceMapConsumer | null {
  let consumer = consumers.get(source);
  if (consumer == null) {
    const code = getGeneratedContents(source);
    if (!code) {
      return null;
    }
    if (typeof code !== "string") {
      throw new Error("expected string");
    }

    let sourceMappingURL = retrieveSourceMapURL(code);
    if (!sourceMappingURL) {
      throw Error("No source map?");
    }

    let sourceMapData: string | RawSourceMap;
    if (reSourceMap.test(sourceMappingURL)) {
      // Support source map URL as a data url
      const rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(",") + 1);
      const ui8 = base64.toByteArray(rawData);
      sourceMapData = arrayToStr(ui8);
      sourceMappingURL = source;
    } else {
      // Support source map URLs relative to the source URL
      //sourceMappingURL = supportRelativeURL(source, sourceMappingURL);
      sourceMapData = getGeneratedContents(sourceMappingURL);
    }

    const rawSourceMap =
      typeof sourceMapData === "string"
        ? JSON.parse(sourceMapData)
        : sourceMapData;
    //console.log("sourceMapData", sourceMapData);
    consumer = new SourceMapConsumer(rawSourceMap);
    consumers.set(source, consumer);
  }
  return consumer;
}

function retrieveSourceMapURL(fileData: string): string | null {
  // Get the URL of the source map
  // tslint:disable-next-line:max-line-length
  const re = /(?:\/\/[@#][ \t]+sourceMappingURL=([^\s'"]+?)[ \t]*$)|(?:\/\*[@#][ \t]+sourceMappingURL=([^\*]+?)[ \t]*(?:\*\/)[ \t]*$)/gm;
  // Keep executing the search to find the *last* sourceMappingURL to avoid
  // picking up sourceMappingURLs from comments, strings, etc.
  let lastMatch, match;
  while ((match = re.exec(fileData))) {
    lastMatch = match;
  }
  if (!lastMatch) {
    return null;
  }
  return lastMatch[1];
}

function mapSourcePosition(position: Position): MappedPosition {
  const consumer = loadConsumer(position.source);
  if (consumer == null) {
    return position;
  }
  return consumer.originalPositionFor(position);
}

// Parses code generated by FormatEvalOrigin(), a function inside V8:
// https://code.google.com/p/v8/source/browse/trunk/src/messages.js
function mapEvalOrigin(origin: string): string {
  // Most eval() calls are in this format
  let match = /^eval at ([^(]+) \((.+):(\d+):(\d+)\)$/.exec(origin);
  if (match) {
    const position = mapSourcePosition({
      source: match[2],
      line: Number(match[3]),
      column: Number(match[4]) - 1
    });
    const pos = [
      position.source,
      position.line,
      Number(position.column) + 1
    ].join(":");
    return `eval at ${match[1]} (${pos})`;
  }

  // Parse nested eval() calls using recursion
  match = /^eval at ([^(]+) \((.+)\)$/.exec(origin);
  if (match) {
    return `eval at ${match[1]} (${mapEvalOrigin(match[2])})`;
  }

  // Make sure we still return useful information if we didn't find anything
  return origin;
}