/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Logging and debugging utilities.
 *
 * @see ../demos/debug.html
 */

goog.provide('goog.debug');

goog.require('goog.array');
goog.require('goog.debug.errorcontext');


/** @define {boolean} Whether logging should be enabled. */
goog.debug.LOGGING_ENABLED =
    goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);


/** @define {boolean} Whether to force "sloppy" stack building. */
goog.debug.FORCE_SLOPPY_STACKS =
    goog.define('goog.debug.FORCE_SLOPPY_STACKS', false);


/**
 * @define {boolean} TODO(user): Remove this hack once bug is resolved.
 */
goog.debug.CHECK_FOR_THROWN_EVENT =
    goog.define('goog.debug.CHECK_FOR_THROWN_EVENT', false);



/**
 * Catches onerror events fired by windows and similar objects.
 * @param {function(Object)} logFunc The function to call with the error
 *    information.
 * @param {boolean=} opt_cancel Whether to stop the error from reaching the
 *    browser.
 * @param {Object=} opt_target Object that fires onerror events.
 * @suppress {strictMissingProperties} onerror is not defined as a property
 *    on Object.
 */
goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
  'use strict';
  var target = opt_target || goog.global;
  var oldErrorHandler = target.onerror;
  var retVal = !!opt_cancel;

  /**
   * New onerror handler for this target. This onerror handler follows the spec
   * according to
   * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors
   * The spec was changed in August 2013 to support receiving column information
   * and an error object for all scripts on the same origin or cross origin
   * scripts with the proper headers. See
   * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
   *
   * @param {string} message The error message. For cross-origin errors, this
   *     will be scrubbed to just "Script error.". For new browsers that have
   *     updated to follow the latest spec, errors that come from origins that
   *     have proper cross origin headers will not be scrubbed.
   * @param {string} url The URL of the script that caused the error. The URL
   *     will be scrubbed to "" for cross origin scripts unless the script has
   *     proper cross origin headers and the browser has updated to the latest
   *     spec.
   * @param {number} line The line number in the script that the error
   *     occurred on.
   * @param {number=} opt_col The optional column number that the error
   *     occurred on. Only browsers that have updated to the latest spec will
   *     include this.
   * @param {Error=} opt_error The optional actual error object for this
   *     error that should include the stack. Only browsers that have updated
   *     to the latest spec will inlude this parameter.
   * @return {boolean} Whether to prevent the error from reaching the browser.
   */
  target.onerror = function(message, url, line, opt_col, opt_error) {
    'use strict';
    if (oldErrorHandler) {
      oldErrorHandler(message, url, line, opt_col, opt_error);
    }
    logFunc({
      message: message,
      fileName: url,
      line: line,
      lineNumber: line,
      col: opt_col,
      error: opt_error
    });
    return retVal;
  };
};


/**
 * Creates a string representing an object and all its properties.
 * @param {Object|null|undefined} obj Object to expose.
 * @param {boolean=} opt_showFn Show the functions as well as the properties,
 *     default is false.
 * @return {string} The string representation of `obj`.
 */
goog.debug.expose = function(obj, opt_showFn) {
  'use strict';
  if (typeof obj == 'undefined') {
    return 'undefined';
  }
  if (obj == null) {
    return 'NULL';
  }
  var str = [];

  for (var x in obj) {
    if (!opt_showFn && typeof obj[x] === 'function') {
      continue;
    }
    var s = x + ' = ';

    try {
      s += obj[x];
    } catch (e) {
      s += '*** ' + e + ' ***';
    }
    str.push(s);
  }
  return str.join('\n');
};


/**
 * Creates a string representing a given primitive or object, and for an
 * object, all its properties and nested objects. NOTE: The output will include
 * Uids on all objects that were exposed. Any added Uids will be removed before
 * returning.
 * @param {*} obj Object to expose.
 * @param {boolean=} opt_showFn Also show properties that are functions (by
 *     default, functions are omitted).
 * @return {string} A string representation of `obj`.
 */
goog.debug.deepExpose = function(obj, opt_showFn) {
  'use strict';
  var str = [];

  // Track any objects where deepExpose added a Uid, so they can be cleaned up
  // before return. We do this globally, rather than only on ancestors so that
  // if the same object appears in the output, you can see it.
  var uidsToCleanup = [];
  var ancestorUids = {};

  var helper = function(obj, space) {
    'use strict';
    var nestspace = space + '  ';

    var indentMultiline = function(str) {
      'use strict';
      return str.replace(/\n/g, '\n' + space);
    };


    try {
      if (obj === undefined) {
        str.push('undefined');
      } else if (obj === null) {
        str.push('NULL');
      } else if (typeof obj === 'string') {
        str.push('"' + indentMultiline(obj) + '"');
      } else if (typeof obj === 'function') {
        str.push(indentMultiline(String(obj)));
      } else if (goog.isObject(obj)) {
        // Add a Uid if needed. The struct calls implicitly adds them.
        if (!goog.hasUid(obj)) {
          uidsToCleanup.push(obj);
        }
        var uid = goog.getUid(obj);
        if (ancestorUids[uid]) {
          str.push('*** reference loop detected (id=' + uid + ') ***');
        } else {
          ancestorUids[uid] = true;
          str.push('{');
          for (var x in obj) {
            if (!opt_showFn && typeof obj[x] === 'function') {
              continue;
            }
            str.push('\n');
            str.push(nestspace);
            str.push(x + ' = ');
            helper(obj[x], nestspace);
          }
          str.push('\n' + space + '}');
          delete ancestorUids[uid];
        }
      } else {
        str.push(obj);
      }
    } catch (e) {
      str.push('*** ' + e + ' ***');
    }
  };

  helper(obj, '');

  // Cleanup any Uids that were added by the deepExpose.
  for (var i = 0; i < uidsToCleanup.length; i++) {
    goog.removeUid(uidsToCleanup[i]);
  }

  return str.join('');
};


/**
 * Recursively outputs a nested array as a string.
 * @param {Array<?>} arr The array.
 * @return {string} String representing nested array.
 */
goog.debug.exposeArray = function(arr) {
  'use strict';
  var str = [];
  for (var i = 0; i < arr.length; i++) {
    if (Array.isArray(arr[i])) {
      str.push(goog.debug.exposeArray(arr[i]));
    } else {
      str.push(arr[i]);
    }
  }
  return '[ ' + str.join(', ') + ' ]';
};


/**
 * Normalizes the error/exception object between browsers.
 * @param {*} err Raw error object.
 * @return {{
 *    message: (?|undefined),
 *    name: (?|undefined),
 *    lineNumber: (?|undefined),
 *    fileName: (?|undefined),
 *    stack: (?|undefined)
 * }} Representation of err as an Object. It will never return err.
 * @suppress {strictMissingProperties} properties not defined on err
 */
goog.debug.normalizeErrorObject = function(err) {
  'use strict';
  var href = goog.getObjectByName('window.location.href');
  if (err == null) {
    err = 'Unknown Error of type "null/undefined"';
  }
  if (typeof err === 'string') {
    return {
      'message': err,
      'name': 'Unknown error',
      'lineNumber': 'Not available',
      'fileName': href,
      'stack': 'Not available'
    };
  }

  var lineNumber, fileName;
  var threwError = false;

  try {
    lineNumber = err.lineNumber || err.line || 'Not available';
  } catch (e) {
    // Firefox 2 sometimes throws an error when accessing 'lineNumber':
    // Message: Permission denied to get property UnnamedClass.lineNumber
    lineNumber = 'Not available';
    threwError = true;
  }

  try {
    fileName = err.fileName || err.filename || err.sourceURL ||
        // $googDebugFname may be set before a call to eval to set the filename
        // that the eval is supposed to present.
        goog.global['$googDebugFname'] || href;
  } catch (e) {
    // Firefox 2 may also throw an error when accessing 'filename'.
    fileName = 'Not available';
    threwError = true;
  }

  var stack = goog.debug.serializeErrorStack_(err);

  // The IE Error object contains only the name and the message.
  // The Safari Error object uses the line and sourceURL fields.
  if (threwError || !err.lineNumber || !err.fileName || !err.stack ||
      !err.message || !err.name) {
    var message = err.message;
    if (message == null) {
      if (err.constructor && err.constructor instanceof Function) {
        var ctorName = err.constructor.name ?
            err.constructor.name :
            goog.debug.getFunctionName(err.constructor);
        message = 'Unknown Error of type "' + ctorName + '"';
        // TODO(user): Remove this hack once bug is resolved.
        if (goog.debug.CHECK_FOR_THROWN_EVENT && ctorName == 'Event') {
          try {
            message = message + ' with Event.type "' + (err.type || '') + '"';
          } catch (e) {
            // Just give up on getting more information out of the error object.
          }
        }
      } else {
        message = 'Unknown Error of unknown type';
      }

      // Avoid TypeError since toString could be missing from the instance
      // (e.g. if created Object.create(null)).
      if (typeof err.toString === 'function' &&
          Object.prototype.toString !== err.toString) {
        message += ': ' + err.toString();
      }
    }
    return {
      'message': message,
      'name': err.name || 'UnknownError',
      'lineNumber': lineNumber,
      'fileName': fileName,
      'stack': stack || 'Not available'
    };
  }
  // Standards error object
  // Typed !Object. Should be a subtype of the return type, but it's not.
  err.stack = stack;

  // Return non-standard error to allow for consistent result (eg. enumerable).
  return {
    'message': err.message,
    'name': err.name,
    'lineNumber': err.lineNumber,
    'fileName': err.fileName,
    'stack': err.stack
  };
};


/**
 * Serialize stack by including the cause chain of the exception if it exists.
 *
 *
 * @param {*} e an exception that may have a cause
 * @param {!Object=} seen set of cause that have already been serialized
 * @return {string}
 * @private
 * @suppress {missingProperties} properties not defined on cause and e
 */
goog.debug.serializeErrorStack_ = function(e, seen) {
  'use strict';
  if (!seen) {
    seen = {};
  }
  seen[goog.debug.serializeErrorAsKey_(e)] = true;

  var stack = e['stack'] || '';

  // Add cause if exists.
  var cause = e.cause;
  if (cause && !seen[goog.debug.serializeErrorAsKey_(cause)]) {
    stack += '\nCaused by: ';
    // Some browsers like Chrome add the error message as the first frame of the
    // stack, In this case we don't need to add it. Note: we don't use
    // String.startsWith method because it might have to be polyfilled.
    if (!cause.stack || cause.stack.indexOf(cause.toString()) != 0) {
      stack += (typeof cause === 'string') ? cause : cause.message + '\n';
    }
    stack += goog.debug.serializeErrorStack_(cause, seen);
  }

  return stack;
};

/**
 * Serialize an error to a string key.
 * @param {*} e an exception
 * @return {string}
 * @private
 */
goog.debug.serializeErrorAsKey_ = function(e) {
  'use strict';
  var keyPrefix = '';

  if (typeof e.toString === 'function') {
    keyPrefix = '' + e;
  }

  return keyPrefix + e['stack'];
};


/**
 * Converts an object to an Error using the object's toString if it's not
 * already an Error, adds a stacktrace if there isn't one, and optionally adds
 * an extra message.
 * @param {*} err The original thrown error, object, or string.
 * @param {string=} opt_message  optional additional message to add to the
 *     error.
 * @return {!Error} If err is an Error, it is enhanced and returned. Otherwise,
 *     it is converted to an Error which is enhanced and returned.
 */
goog.debug.enhanceError = function(err, opt_message) {
  'use strict';
  var error;
  if (!(err instanceof Error)) {
    error = Error(err);
    if (Error.captureStackTrace) {
      // Trim this function off the call stack, if we can.
      Error.captureStackTrace(error, goog.debug.enhanceError);
    }
  } else {
    error = err;
  }

  if (!error.stack) {
    error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);
  }
  if (opt_message) {
    // find the first unoccupied 'messageX' property
    var x = 0;
    while (error['message' + x]) {
      ++x;
    }
    error['message' + x] = String(opt_message);
  }
  return error;
};


/**
 * Converts an object to an Error using the object's toString if it's not
 * already an Error, adds a stacktrace if there isn't one, and optionally adds
 * context to the Error, which is reported by the closure error reporter.
 * @param {*} err The original thrown error, object, or string.
 * @param {!Object<string, string>=} opt_context Key-value context to add to the
 *     Error.
 * @return {!Error} If err is an Error, it is enhanced and returned. Otherwise,
 *     it is converted to an Error which is enhanced and returned.
 */
goog.debug.enhanceErrorWithContext = function(err, opt_context) {
  'use strict';
  var error = goog.debug.enhanceError(err);
  if (opt_context) {
    for (var key in opt_context) {
      goog.debug.errorcontext.addErrorContext(error, key, opt_context[key]);
    }
  }
  return error;
};


/**
 * Gets the current stack trace. Simple and iterative - doesn't worry about
 * catching circular references or getting the args.
 * @param {number=} opt_depth Optional maximum depth to trace back to.
 * @return {string} A string with the function names of all functions in the
 *     stack, separated by \n.
 * @suppress {es5Strict}
 */
goog.debug.getStacktraceSimple = function(opt_depth) {
  'use strict';
  if (!goog.debug.FORCE_SLOPPY_STACKS) {
    var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple);
    if (stack) {
      return stack;
    }
    // NOTE: browsers that have strict mode support also have native "stack"
    // properties.  Fall-through for legacy browser support.
  }

  var sb = [];
  var fn = arguments.callee.caller;
  var depth = 0;

  while (fn && (!opt_depth || depth < opt_depth)) {
    sb.push(goog.debug.getFunctionName(fn));
    sb.push('()\n');

    try {
      fn = fn.caller;
    } catch (e) {
      sb.push('[exception trying to get caller]\n');
      break;
    }
    depth++;
    if (depth >= goog.debug.MAX_STACK_DEPTH) {
      sb.push('[...long stack...]');
      break;
    }
  }
  if (opt_depth && depth >= opt_depth) {
    sb.push('[...reached max depth limit...]');
  } else {
    sb.push('[end]');
  }

  return sb.join('');
};


/**
 * Max length of stack to try and output
 * @type {number}
 */
goog.debug.MAX_STACK_DEPTH = 50;


/**
 * @param {Function} fn The function to start getting the trace from.
 * @return {?string}
 * @private
 */
goog.debug.getNativeStackTrace_ = function(fn) {
  'use strict';
  var tempErr = new Error();
  if (Error.captureStackTrace) {
    Error.captureStackTrace(tempErr, fn);
    return String(tempErr.stack);
  } else {
    // IE10, only adds stack traces when an exception is thrown.
    try {
      throw tempErr;
    } catch (e) {
      tempErr = e;
    }
    var stack = tempErr.stack;
    if (stack) {
      return String(stack);
    }
  }
  return null;
};


/**
 * Gets the current stack trace, either starting from the caller or starting
 * from a specified function that's currently on the call stack.
 * @param {?Function=} fn If provided, when collecting the stack trace all
 *     frames above the topmost call to this function, including that call,
 *     will be left out of the stack trace.
 * @return {string} Stack trace.
 * @suppress {es5Strict}
 */
goog.debug.getStacktrace = function(fn) {
  'use strict';
  var stack;
  if (!goog.debug.FORCE_SLOPPY_STACKS) {
    // Try to get the stack trace from the environment if it is available.
    var contextFn = fn || goog.debug.getStacktrace;
    stack = goog.debug.getNativeStackTrace_(contextFn);
  }
  if (!stack) {
    // NOTE: browsers that have strict mode support also have native "stack"
    // properties. This function will throw in strict mode.
    stack = goog.debug.getStacktraceHelper_(fn || arguments.callee.caller, []);
  }
  return stack;
};


/**
 * Private helper for getStacktrace().
 * @param {?Function} fn If provided, when collecting the stack trace all
 *     frames above the topmost call to this function, including that call,
 *     will be left out of the stack trace.
 * @param {Array<!Function>} visited List of functions visited so far.
 * @return {string} Stack trace starting from function fn.
 * @suppress {es5Strict}
 * @private
 */
goog.debug.getStacktraceHelper_ = function(fn, visited) {
  'use strict';
  var sb = [];

  // Circular reference, certain functions like bind seem to cause a recursive
  // loop so we need to catch circular references
  if (goog.array.contains(visited, fn)) {
    sb.push('[...circular reference...]');

    // Traverse the call stack until function not found or max depth is reached
  } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
    sb.push(goog.debug.getFunctionName(fn) + '(');
    var args = fn.arguments;
    // Args may be null for some special functions such as host objects or eval.
    for (var i = 0; args && i < args.length; i++) {
      if (i > 0) {
        sb.push(', ');
      }
      var argDesc;
      var arg = args[i];
      switch (typeof arg) {
        case 'object':
          argDesc = arg ? 'object' : 'null';
          break;

        case 'string':
          argDesc = arg;
          break;

        case 'number':
          argDesc = String(arg);
          break;

        case 'boolean':
          argDesc = arg ? 'true' : 'false';
          break;

        case 'function':
          argDesc = goog.debug.getFunctionName(arg);
          argDesc = argDesc ? argDesc : '[fn]';
          break;

        case 'undefined':
        default:
          argDesc = typeof arg;
          break;
      }

      if (argDesc.length > 40) {
        argDesc = argDesc.slice(0, 40) + '...';
      }
      sb.push(argDesc);
    }
    visited.push(fn);
    sb.push(')\n');

    try {
      sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited));
    } catch (e) {
      sb.push('[exception trying to get caller]\n');
    }

  } else if (fn) {
    sb.push('[...long stack...]');
  } else {
    sb.push('[end]');
  }
  return sb.join('');
};


/**
 * Gets a function name
 * @param {Function} fn Function to get name of.
 * @return {string} Function's name.
 */
goog.debug.getFunctionName = function(fn) {
  'use strict';
  if (goog.debug.fnNameCache_[fn]) {
    return goog.debug.fnNameCache_[fn];
  }

  // Heuristically determine function name based on code.
  var functionSource = String(fn);
  if (!goog.debug.fnNameCache_[functionSource]) {
    var matches = /function\s+([^\(]+)/m.exec(functionSource);
    if (matches) {
      var method = matches[1];
      goog.debug.fnNameCache_[functionSource] = method;
    } else {
      goog.debug.fnNameCache_[functionSource] = '[Anonymous]';
    }
  }

  return goog.debug.fnNameCache_[functionSource];
};


/**
 * Makes whitespace visible by replacing it with printable characters.
 * This is useful in finding diffrences between the expected and the actual
 * output strings of a testcase.
 * @param {string} string whose whitespace needs to be made visible.
 * @return {string} string whose whitespace is made visible.
 */
goog.debug.makeWhitespaceVisible = function(string) {
  'use strict';
  return string.replace(/ /g, '[_]')
      .replace(/\f/g, '[f]')
      .replace(/\n/g, '[n]\n')
      .replace(/\r/g, '[r]')
      .replace(/\t/g, '[t]');
};


/**
 * Returns the type of a value. If a constructor is passed, and a suitable
 * string cannot be found, 'unknown type name' will be returned.
 *
 * <p>Forked rather than moved from {@link goog.asserts.getType_}
 * to avoid adding a dependency to goog.asserts.
 * @param {*} value A constructor, object, or primitive.
 * @return {string} The best display name for the value, or 'unknown type name'.
 */
goog.debug.runtimeType = function(value) {
  'use strict';
  if (value instanceof Function) {
    return value.displayName || value.name || 'unknown type name';
  } else if (value instanceof Object) {
    return /** @type {string} */ (value.constructor.displayName) ||
        value.constructor.name || Object.prototype.toString.call(value);
  } else {
    return value === null ? 'null' : typeof value;
  }
};


/**
 * Hash map for storing function names that have already been looked up.
 * @type {Object}
 * @private
 */
goog.debug.fnNameCache_ = {};


/**
 * Private internal function to support goog.debug.freeze.
 * @param {T} arg
 * @return {T}
 * @template T
 * @private
 */
goog.debug.freezeInternal_ = goog.DEBUG && Object.freeze || function(arg) {
  'use strict';
  return arg;
};


/**
 * Freezes the given object, but only in debug mode (and in browsers that
 * support it).  Note that this is a shallow freeze, so for deeply nested
 * objects it must be called at every level to ensure deep immutability.
 * @param {T} arg
 * @return {T}
 * @template T
 */
goog.debug.freeze = function(arg) {
  'use strict';
  // NOTE: this compiles to nothing, but hides the possible side effect of
  // freezeInternal_ from the compiler so that the entire call can be
  // removed if the result is not used.
  return {
    valueOf: function() {
      'use strict';
      return goog.debug.freezeInternal_(arg);
    }
  }.valueOf();
};
