// Significant portion of this code is copy-pasted from the node.js source
// Modifications consist primarily of removing dependencies on v8 natives and adding typings

// Original license:
/*
 * Copyright Joyent, Inc. and other Node contributors. All rights reserved.
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

import {
  Constructable,
  isArrayIndex,
  Primitive,
} from '@aurelia/kernel';
import {
  Boolean_valueOf,
  colors,
  Date_getTime,
  Date_toISOString,
  Date_toString,
  defineProperties,
  defineProperty,
  Error_toString,
  escapeAndQuoteString,
  escapeString,
  getOwnNonIndexProperties,
  getOwnPropertyDescriptor,
  getOwnPropertyDescriptors,
  getOwnPropertyNames,
  getOwnPropertySymbols,
  getPrototypeOf,
  hasOwnProperty,
  isAnyArrayBuffer,
  isArgumentsObject,
  isArrayBuffer,
  isBooleanObject,
  isBoxedPrimitive,
  isDataView,
  isDate,
  isError,
  isFloat32Array,
  isFloat64Array,
  isFunction,
  isInt16Array,
  isInt32Array,
  isInt8Array,
  isMap,
  isMapIterator,
  isNumber,
  isNumberObject,
  isObject,
  isPromise,
  isRegExp,
  isSet,
  isSetIterator,
  isString,
  isStringObject,
  isSymbol,
  isTypedArray,
  isUint16Array,
  isUint32Array,
  isUint8Array,
  isUint8ClampedArray,
  isUndefined,
  isWeakMap,
  isWeakSet,
  join,
  Map_entries,
  Number_valueOf,
  Object_assign,
  Object_freeze,
  Object_is,
  Object_keys,
  Object_toString,
  propertyIsEnumerable,
  RegExp_toString,
  removeColors,
  Set_values,
  String_valueOf,
  Symbol_valueOf,
  // truncate,
  TypedArray,
  TypedArrayConstructor,
} from './util';
import { PLATFORM } from './test-context';

_START_CONST_ENUM();
const enum Char {
  Null           = 0x00,
  Backspace      = 0x08,
  Tab            = 0x09,
  LineFeed       = 0x0A,
  VerticalTab    = 0x0B,
  FormFeed       = 0x0C,
  CarriageReturn = 0x0D,
  Space          = 0x20,
  Exclamation    = 0x21,
  DoubleQuote    = 0x22,
  Dollar         = 0x24,
  Percent        = 0x25,
  Ampersand      = 0x26,
  SingleQuote    = 0x27,
  OpenParen      = 0x28,
  CloseParen     = 0x29,
  Asterisk       = 0x2A,
  Plus           = 0x2B,
  Comma          = 0x2C,
  Minus          = 0x2D,
  Dot            = 0x2E,
  Slash          = 0x2F,
  Semicolon      = 0x3B,
  Backtick       = 0x60,
  OpenBracket    = 0x5B,
  Backslash      = 0x5C,
  CloseBracket   = 0x5D,
  Caret          = 0x5E,
  Underscore     = 0x5F,
  OpenBrace      = 0x7B,
  Bar            = 0x7C,
  CloseBrace     = 0x7D,
  Colon          = 0x3A,
  LessThan       = 0x3C,
  Equals         = 0x3D,
  GreaterThan    = 0x3E,
  Question       = 0x3F,

  Zero   = 0x30,
  One    = 0x31,
  Two    = 0x32,
  Three  = 0x33,
  Four   = 0x34,
  Five   = 0x35,
  Six    = 0x36,
  Seven  = 0x37,
  Eight  = 0x38,
  Nine   = 0x39,

  UpperA = 0x41,
  UpperB = 0x42,
  UpperC = 0x43,
  UpperD = 0x44,
  UpperE = 0x45,
  UpperF = 0x46,
  UpperG = 0x47,
  UpperH = 0x48,
  UpperI = 0x49,
  UpperJ = 0x4A,
  UpperK = 0x4B,
  UpperL = 0x4C,
  UpperM = 0x4D,
  UpperN = 0x4E,
  UpperO = 0x4F,
  UpperP = 0x50,
  UpperQ = 0x51,
  UpperR = 0x52,
  UpperS = 0x53,
  UpperT = 0x54,
  UpperU = 0x55,
  UpperV = 0x56,
  UpperW = 0x57,
  UpperX = 0x58,
  UpperY = 0x59,
  UpperZ = 0x5A,

  LowerA  = 0x61,
  LowerB  = 0x62,
  LowerC  = 0x63,
  LowerD  = 0x64,
  LowerE  = 0x65,
  LowerF  = 0x66,
  LowerG  = 0x67,
  LowerH  = 0x68,
  LowerI  = 0x69,
  LowerJ  = 0x6A,
  LowerK  = 0x6B,
  LowerL  = 0x6C,
  LowerM  = 0x6D,
  LowerN  = 0x6E,
  LowerO  = 0x6F,
  LowerP  = 0x70,
  LowerQ  = 0x71,
  LowerR  = 0x72,
  LowerS  = 0x73,
  LowerT  = 0x74,
  LowerU  = 0x75,
  LowerV  = 0x76,
  LowerW  = 0x77,
  LowerX  = 0x78,
  LowerY  = 0x79,
  LowerZ  = 0x7A
}
_END_CONST_ENUM();

/* eslint-disable max-lines-per-function, @typescript-eslint/ban-types */

let maxStack_ErrorName: string;
let maxStack_ErrorMessage: string;

function isStackOverflowError(err: Error): boolean {
  if (maxStack_ErrorMessage === undefined) {
    try {
      // eslint-disable-next-line no-inner-declarations
      function overflowStack(): void { overflowStack(); }
      overflowStack();
    } catch (err) {
      maxStack_ErrorMessage = (err as Error).message;
      maxStack_ErrorName = (err as Error).name;
    }
  }

  return (
    err.name === maxStack_ErrorName
    && err.message === maxStack_ErrorMessage
  );
}

export interface IInspectOptions {
  showHidden: boolean;
  depth: number;
  colors: boolean;
  customInspect: boolean;
  showProxy: boolean;
  maxArrayLength: number;
  breakLength: number;
  compact: boolean;
  sorted: boolean;
  getters: boolean;

  userOptions?: Partial<IInspectContext>;

  stylize(str: string, styleType: keyof typeof styles): string;
}

export interface IInspectContext extends IInspectOptions {
  budget: Record<number, number>;
  indentationLvl: number;
  seen: any[];
  currentDepth: number;
  stop?: boolean;
}

const defaultInspectOptions: Readonly<IInspectOptions> = Object_freeze(
  {
    showHidden: false,
    depth: 2,
    colors: true,
    customInspect: true,
    showProxy: false,
    maxArrayLength: 100,
    breakLength: 60,
    compact: true,
    sorted: false,
    getters: false,

    userOptions: void 0,

    stylize: stylizeWithColor,
  }
);

const mandatoryInspectKeys = Object_keys(defaultInspectOptions) as (keyof IInspectOptions)[];

function getUserOptions(ctx: Partial<IInspectOptions>): IInspectOptions {
  const obj: Partial<IInspectOptions> = {};
  for (const key of mandatoryInspectKeys) {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore // TODO: https://github.com/microsoft/TypeScript/issues/31904
    obj[key] = ctx[key];
  }

  if (ctx.userOptions !== void 0) {
    Object_assign(obj, ctx.userOptions);
  }

  return obj as IInspectOptions;
}

function getInspectContext(ctx: Partial<IInspectOptions>): IInspectContext {
  const obj: IInspectContext = {
    ...defaultInspectOptions,
    budget: {},
    indentationLvl: 0,
    seen: [],
    currentDepth: 0,
    stylize: ctx.colors ? stylizeWithColor : stylizeNoColor,
  };

  for (const key of mandatoryInspectKeys) {
    if (hasOwnProperty(ctx, key)) {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore // TODO: https://github.com/microsoft/TypeScript/issues/31904
      obj[key] = ctx[key];
    }
  }

  if (obj.userOptions === void 0) {
    obj.userOptions = ctx;
  }

  return obj;
}

const styles = Object_freeze(
  {
    special: 'cyan',
    number: 'yellow',
    boolean: 'yellow',
    undefined: 'grey',
    null: 'bold',
    string: 'green',
    symbol: 'green',
    date: 'magenta',
    regexp: 'red',
  } as const,
);

interface IOperatorText {
  deepStrictEqual: string;
  strictEqual: string;
  strictEqualObject: string;
  deepEqual: string;
  equal: string;
  notDeepStrictEqual: string;
  notStrictEqual: string;
  notStrictEqualObject: string;
  notDeepEqual: string;
  notEqual: string;
  notIdentical: string;
}

const operatorText: Readonly<IOperatorText> = Object_freeze(
  {
    deepStrictEqual: 'Expected values to be strictly deep-equal:',
    strictEqual: 'Expected values to be strictly equal:',
    strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
    deepEqual: 'Expected values to be loosely deep-equal:',
    equal: 'Expected values to be loosely equal:',
    notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
    notStrictEqual: 'Expected "actual" to be strictly unequal to:',
    notStrictEqualObject: 'Expected "actual" not to be reference-equal to "expected":',
    notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
    notEqual: 'Expected "actual" to be loosely unequal to:',
    notIdentical: 'Values identical but not reference-equal:',
  },
);

export const customInspectSymbol = Symbol.for('customInspect');

function stylizeWithColor(str: string, styleType: keyof typeof styles): string {
  const style = styles[styleType];

  if (isString(style)) {
    return colors[style](str);
  } else {
    return str;
  }
}

function stylizeNoColor(str: string, _styleType: keyof typeof styles): string {
  return str;
}

export interface IAssertionErrorOpts {
  actual: any;
  expected: any;
  operator: keyof IOperatorText;
  message?: string | Error;
  stackStartFn?: Function;
}

export class AssertionError extends Error {
  public code: string;
  public actual: any;
  public expected: any;
  public operator!: keyof IOperatorText;
  public generatedMessage: boolean;

  public constructor(options: IAssertionErrorOpts) {
    const {
      actual,
      expected,
      message,
      operator,
      stackStartFn
    } = options;

    const limit = Error.stackTraceLimit;
    Error.stackTraceLimit = 0;

    let prefix = message == null ? '' : `${message} - `;

    if (operator === 'deepStrictEqual' || operator === 'strictEqual') {
      super(`${prefix}${createErrDiff(actual, expected, operator)}`);
    } else if (
      operator === 'notDeepStrictEqual'
      || operator === 'notStrictEqual'
    ) {
      let base = operatorText[operator];
      // eslint-disable-next-line prefer-const
      let res = inspectValue(actual).split('\n');
      if (
        operator === 'notStrictEqual'
        && isObject(actual)
      ) {
        base = operatorText.notStrictEqualObject;
      }

      if (res.length > 30) {
        res[26] = colors.blue('...');
        while (res.length > 27) {
          res.pop();
        }
      }

      if (res.length === 1) {
        super(`${prefix}${base} ${res[0]}`);
      } else {
        super(`${prefix}${base}\n\n${join(res, '\n')}\n`);
      }
    } else {
      let res = inspectValue(actual);
      let other = '';
      const knownOperators = operatorText[operator];
      if (operator === 'notDeepEqual' || operator === 'notEqual') {
        res = `${operatorText[operator]}\n\n${res}`;
        if (res.length > 1024) {
          res = `${res.slice(0, 1021)}...`;
        }
      } else {
        other = `${inspectValue(expected)}`;
        if (res.length > 512) {
          res = `${res.slice(0, 509)}...`;
        }
        if (other.length > 512) {
          other = `${other.slice(0, 509)}...`;
        }
        if (operator === 'deepEqual' || operator === 'equal') {
          res = `${knownOperators}\n\n${res}\n\nshould equal\n\n`;
        } else {
          other = ` ${operator} ${other}`;
        }
      }
      if (!operator) {
        other = '';
        res = '';
        prefix = prefix.slice(0, -3);
      }
      super(`${prefix}${res}${other}`);
    }

    Error.stackTraceLimit = limit;

    this.generatedMessage = !message || message === 'Failed';
    defineProperty(this, 'name', {
      value: 'AssertionError [ERR_ASSERTION]',
      enumerable: false,
      writable: true,
      configurable: true
    });
    this.code = 'ERR_ASSERTION';
    this.actual = actual;
    this.expected = expected;
    this.operator = operator;
    if (typeof Error.captureStackTrace === 'function') {
      Error.captureStackTrace(this, stackStartFn);
      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
      this.stack;
    } else {
      // eslint-disable-next-line @typescript-eslint/no-unused-expressions
      Error().stack;
    }
    this.name = 'AssertionError';
  }

  public toString(): string {
    return `${this.name} [${this.code}]: ${this.message}`;
  }

  public [customInspectSymbol](recurseTimes: number, ctx: IInspectContext): string {
    return inspect(
      this,
      {
        ...ctx,
        customInspect: false,
        depth: 0,
      },
    );
  }
}

const kMaxShortLength = 10;

function createErrDiff(actual: any, expected: any, operator: keyof IOperatorText): string {
  let other = '';
  let res = '';
  let lastPos = 0;
  let end = '';
  let skipped = false;
  const actualInspected = inspectValue(actual);
  const actualLines = actualInspected.split('\n');
  const expectedLines = inspectValue(expected).split('\n');

  let i = 0;
  let indicator = '';

  if (
    operator === 'strictEqual'
    && isObject(actual)
    && isObject(expected)
  ) {
    operator = 'strictEqualObject';
  }

  if (
    actualLines.length === 1
    && expectedLines.length === 1
    && actualLines[0] !== expectedLines[0]
  ) {
    const inputLength = actualLines[0].length + expectedLines[0].length;
    if (inputLength <= kMaxShortLength) {
      if (
        !isObject(actual)
        && !isObject(expected)
        && (actual !== 0 || expected !== 0)
      ) {
        return `${operatorText[operator]}\n\n${actualLines[0]} !== ${expectedLines[0]}\n`;
      }
    } else if (operator !== 'strictEqualObject' && inputLength < 80) {
      while (actualLines[0][i] === expectedLines[0][i]) {
        i++;
      }
      if (i > 2) {
        indicator = `\n  ${' '.repeat(i)}^`;
        i = 0;
      }
    }
  }

  let a = actualLines[actualLines.length - 1];
  let b = expectedLines[expectedLines.length - 1];
  while (a === b) {
    if (i++ < 2) {
      end = `\n  ${a}${end}`;
    } else {
      other = a;
    }
    actualLines.pop();
    expectedLines.pop();
    if (actualLines.length === 0 || expectedLines.length === 0) {
      break;
    }
    a = actualLines[actualLines.length - 1];
    b = expectedLines[expectedLines.length - 1];
  }

  const maxLines = Math.max(actualLines.length, expectedLines.length);
  if (maxLines === 0) {
    const $actualLines = actualInspected.split('\n');
    if ($actualLines.length > 30) {
      $actualLines[26] = colors.blue('...');
      while ($actualLines.length > 27) {
        $actualLines.pop();
      }
    }

    return `${operatorText.notIdentical}\n\n${join($actualLines, '\n')}\n`;
  }

  if (i > 3) {
    end = `\n${colors.blue('...')}${end}`;
    skipped = true;
  }
  if (other !== '') {
    end = `\n  ${other}${end}`;
    other = '';
  }

  let printedLines = 0;
  const msg = `${operatorText[operator]}\n${colors.green('+ actual')} ${colors.red('- expected')}`;
  const skippedMsg = ` ${colors.blue('...')} Lines skipped`;

  for (i = 0; i < maxLines; i++) {
    const cur = i - lastPos;
    if (actualLines.length < i + 1) {
      if (cur > 1 && i > 2) {
        if (cur > 4) {
          res += `\n${colors.blue('...')}`;
          skipped = true;
        } else if (cur > 3) {
          res += `\n  ${expectedLines[i - 2]}`;
          printedLines++;
        }
        res += `\n  ${expectedLines[i - 1]}`;
        printedLines++;
      }
      lastPos = i;
      other += `\n${colors.red('-')} ${expectedLines[i]}`;
      printedLines++;
    } else if (expectedLines.length < i + 1) {
      if (cur > 1 && i > 2) {
        if (cur > 4) {
          res += `\n${colors.blue('...')}`;
          skipped = true;
        } else if (cur > 3) {
          res += `\n  ${actualLines[i - 2]}`;
          printedLines++;
        }
        res += `\n  ${actualLines[i - 1]}`;
        printedLines++;
      }
      lastPos = i;
      res += `\n${colors.green('+')} ${actualLines[i]}`;
      printedLines++;
    } else {
      const expectedLine = expectedLines[i];
      let actualLine = actualLines[i];
      let divergingLines = (
        actualLine !== expectedLine && (!actualLine.endsWith(',')
        || actualLine.slice(0, -1) !== expectedLine)
      );
      if (
        divergingLines
        && expectedLine.endsWith(',')
        && expectedLine.slice(0, -1) === actualLine
      ) {
        divergingLines = false;
        actualLine += ',';
      }
      if (divergingLines) {
        if (cur > 1 && i > 2) {
          if (cur > 4) {
            res += `\n${colors.blue('...')}`;
            skipped = true;
          } else if (cur > 3) {
            res += `\n  ${actualLines[i - 2]}`;
            printedLines++;
          }
          res += `\n  ${actualLines[i - 1]}`;
          printedLines++;
        }
        lastPos = i;
        res += `\n${colors.green('+')} ${actualLine}`;
        other += `\n${colors.red('-')} ${expectedLine}`;
        printedLines += 2;
      } else {
        res += other;
        other = '';
        if (cur === 1 || i === 0) {
          res += `\n  ${actualLine}`;
          printedLines++;
        }
      }
    }
    if (printedLines > 1000 && i < maxLines - 2) {
      return `${msg}${skippedMsg}\n${res}\n${colors.blue('...')}${other}\n${colors.blue('...')}`;
    }
  }

  return `${msg}${skipped ? skippedMsg : ''}\n${res}${other}${end}${indicator}`;
}

const kObjectType = 0;
const kArrayType = 1;
const kArrayExtrasType = 2;

const idStart = new Int8Array(0x80);
const idPart = new Int8Array(0x80);

for (let i = 0; i < 0x80; ++i) {
  if (
    i === Char.Dollar
    || i === Char.Underscore
    || (i >= Char.UpperA && i <= Char.UpperZ)
    || (i >= Char.LowerA && i <= Char.LowerZ)
  ) {
    idStart[i] = idPart[i] = 1;
  } else if (
    i >= Char.One && i <= Char.Nine
  ) {
    idPart[i] = 1;
  }
}

function isValidIdentifier(str: string): boolean {
  if (idStart[str.charCodeAt(0)] !== 1) {
    return false;
  }

  const { length } = str;
  for (let i = 1; i < length; ++i) {
    if (idPart[str.charCodeAt(i)] !== 1) {
      return false;
    }
  }

  return true;
}

const readableRegExps: Record<number, RegExp> = {};

const kMinLineLength = 16;

// Constants to map the iterator state.
const kWeak = 0;
const kIterator = 1;
const kMapEntries = 2;

function groupArrayElements(ctx: IInspectContext, output: string[]): string[] {
  let totalLength = 0;
  let maxLength = 0;
  let i = 0;
  const dataLen = new Array(output.length);
  // Calculate the total length of all output entries and the individual max
  // entries length of all output entries. We have to remove colors first,
  // otherwise the length would not be calculated properly.

  for (; i < output.length; i++) {
    const len = ctx.colors ? removeColors(output[i]).length : output[i].length;
    dataLen[i] = len;
    totalLength += len;
    if (maxLength < len) {
      maxLength = len;
    }
  }

  // Add two to `maxLength` as we add a single whitespace character plus a comma
  // in-between two entries.
  const actualMax = maxLength + 2;
  // Check if at least three entries fit next to each other and prevent grouping
  // of arrays that contains entries of very different length (i.e., if a single
  // entry is longer than 1/5 of all other entries combined). Otherwise the
  // space in-between small entries would be enormous.
  if (
    actualMax * 3 + ctx.indentationLvl < ctx.breakLength
    && (totalLength / maxLength > 5 || maxLength <= 6)
  ) {
    const approxCharHeights = 2.5;
    const bias = 1;
    // Dynamically check how many columns seem possible.
    const columns = Math.min(
      // Ideally a square should be drawn. We expect a character to be about 2.5
      // times as high as wide. This is the area formula to calculate a square
      // which contains n rectangles of size `actualMax * approxCharHeights`.
      // Divide that by `actualMax` to receive the correct number of columns.
      // The added bias slightly increases the columns for short entries.
      Math.round(
        Math.sqrt(
          approxCharHeights * (actualMax - bias) * output.length
        )
        / (actualMax - bias)
      ),
      // Limit array grouping for small `compact` modes as the user requested
      // minimal grouping.
      (ctx.compact as unknown as number) * 3,
      // Limit the columns to a maximum of ten.
      10
    );

    // Return with the original output if no grouping should happen.
    if (columns <= 1) {
      return output;
    }

    // Calculate the maximum length of all entries that are visible in the first
    // column of the group.
    const tmp = [];
    let firstLineMaxLength: number = dataLen[0];
    for (i = columns; i < dataLen.length; i += columns) {
      if (dataLen[i] > firstLineMaxLength) {
        firstLineMaxLength = dataLen[i];
      }
    }

    // Each iteration creates a single line of grouped entries.
    for (i = 0; i < output.length; i += columns) {
      // Calculate extra color padding in case it's active. This has to be done
      // line by line as some lines might contain more colors than others.
      let colorPadding = output[i].length - dataLen[i];
      // Add padding to the first column of the output.
      let str = output[i].padStart(firstLineMaxLength + colorPadding, ' ');
      // The last lines may contain less entries than columns.
      const max = Math.min(i + columns, output.length);
      for (let j = i + 1; j < max; j++) {
        colorPadding = output[j].length - dataLen[j];
        str += `, ${output[j].padStart(maxLength + colorPadding, ' ')}`;
      }
      tmp.push(str);
    }
    output = tmp;
  }
  return output;
}

function handleMaxCallStackSize(
  ctx: IInspectContext,
  err: Error,
  constructor: string,
  tag: string,
  indentationLvl: number,
): string {
  if (isStackOverflowError(err)) {
    ctx.seen.pop();
    ctx.indentationLvl = indentationLvl;
    return ctx.stylize(
      `[${getCtxStyle(constructor, tag)}: Inspection interrupted prematurely. Maximum call stack size exceeded.]`,
      'special'
    );
  }
  throw err;
}

const typedArrayKeys = Object_freeze([
  'BYTES_PER_ELEMENT',
  'length',
  'byteLength',
  'byteOffset',
  'buffer'
]) as [
  'BYTES_PER_ELEMENT',
  'length',
  'byteLength',
  'byteOffset',
  'buffer'
];

function entriesToArray(value: IterableIterator<[any, any]>): any[] {
  const ret = [];
  for (const [k, v] of value) {
    ret.push(k, v);
  }
  return ret;
}

function isBelowBreakLength(
  ctx: IInspectContext,
  output: string[],
  start: number,
): boolean {
  let totalLength = output.length + start;
  if (totalLength + output.length > ctx.breakLength) {
    return false;
  }
  for (let i = 0; i < output.length; i++) {
    if (ctx.colors) {
      totalLength += removeColors(output[i]).length;
    } else {
      totalLength += output[i].length;
    }
    if (totalLength > ctx.breakLength) {
      return false;
    }
  }
  return true;
}

function reduceToSingleString(
  ctx: IInspectContext,
  output: string[],
  base: string,
  braces: [string, string],
  combine: boolean = false,
): string {
  if (ctx.compact !== true) {
    if (combine) {
      const start = (
        output.length
        + ctx.indentationLvl
        + braces[0].length
        + base.length
        + 10
      );
      if (isBelowBreakLength(ctx, output, start)) {
        return `${base ? `${base} ` : ''}${braces[0]} ${join(output, ', ')} ${braces[1]}`;
      }
    }
    const indent = `\n${' '.repeat(ctx.indentationLvl)}`;
    return `${base ? `${base} ` : ''}${braces[0]}${indent}  ${join(output, `,${indent}  `)}${indent}${braces[1]}`;
  }
  if (isBelowBreakLength(ctx, output, 0)) {
    return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ${braces[1]}`;
  }
  const indentation = ' '.repeat(ctx.indentationLvl);

  const ln = base === '' && braces[0].length === 1
    ? ' '
    : `${base ? ` ${base}` : ''}\n${indentation}  `;

  return `${braces[0]}${ln}${join(output, `,\n${indentation}  `)} ${braces[1]}`;
}

function getConstructorName(
  obj: any,
  ctx: IInspectContext,
): string | null {
  let firstProto;
  while (obj) {
    const descriptor = getOwnPropertyDescriptor(obj, 'constructor');
    if (
      !isUndefined(descriptor)
      && isFunction(descriptor.value)
      && descriptor.value.name !== ''
    ) {
      return descriptor.value.name;
    }

    obj = getPrototypeOf(obj);
    if (firstProto === void 0) {
      firstProto = obj;
    }
  }

  if (firstProto === null) {
    return null;
  }

  const newCtx: IInspectContext = {
    ...ctx,
    customInspect: false,
  };
  return `<${inspect(firstProto, newCtx)}>`;
}

function getEmptyFormatArray(): string[] {
  return [];
}

function getPrefix(
  constructor: string | null,
  tag: string,
  fallback?: string,
): string {
  if (constructor === null) {
    if (tag !== '') {
      return `[${fallback}: null prototype] [${tag}] `;
    }
    return `[${fallback}: null prototype] `;
  }

  if (tag !== '' && constructor !== tag) {
    return `${constructor} [${tag}] `;
  }
  return `${constructor} `;
}

const getBoxedValue = formatPrimitive.bind(null, stylizeNoColor);

function getKeys(
  value: any,
  showHidden: boolean,
): PropertyKey[] {
  let keys: PropertyKey[];
  const symbols = getOwnPropertySymbols(value);
  if (showHidden) {
    keys = getOwnPropertyNames(value);
    if (symbols.length !== 0) {
      keys.push(...symbols);
    }
  } else {
    keys = Object_keys(value);
    if (symbols.length !== 0) {
      keys.push(...symbols.filter((key) => propertyIsEnumerable(value, key)));
    }
  }
  return keys;
}

function getCtxStyle(constructor: string | null, tag: string): string {
  return constructor || tag || 'Object';
}

const typedConstructorMap = Object_freeze(
  [
    [isUint8Array, Uint8Array],
    [isUint8ClampedArray, Uint8ClampedArray],
    [isUint16Array, Uint16Array],
    [isUint32Array, Uint32Array],
    [isInt8Array, Int8Array],
    [isInt16Array, Int16Array],
    [isInt32Array, Int32Array],
    [isFloat32Array, Float32Array],
    [isFloat64Array, Float64Array],
  ],
);

const typedConstructorCount = typedConstructorMap.length;

function findTypedConstructor(value: unknown): TypedArrayConstructor {
  for (let i = 0; i < typedConstructorCount; ++i) {
    const [isType, Type] = typedConstructorMap[i];
    if ((isType as (value: unknown) => boolean)(value)) {
      return Type as TypedArrayConstructor;
    }
  }
  return (void 0)!;
}

function setIteratorBraces(type: string, tag: string): [string, string] {
  if (tag !== `${type} Iterator`) {
    if (tag !== '') {
      tag += '] [';
    }
    tag += `${type} Iterator`;
  }
  return [`[${tag}] {`, '}'];
}

let lazyNullPrototypeCache: Map<Constructable, Constructable>;
// Creates a subclass and name
// the constructor as `${clazz} : null prototype`
function clazzWithNullPrototype(clazz: Constructable, name: string): Constructable {
  if (lazyNullPrototypeCache === undefined) {
    lazyNullPrototypeCache = new Map();
  } else {
    const cachedClass = lazyNullPrototypeCache.get(clazz);
    if (cachedClass !== undefined) {
      return cachedClass;
    }
  }
  class NullPrototype extends clazz {
    public get [Symbol.toStringTag](): string {
      return '';
    }
  }
  defineProperty(
    NullPrototype.prototype.constructor,
    'name',
    { value: `[${name}: null prototype]` },
  );
  lazyNullPrototypeCache.set(clazz, NullPrototype);
  return NullPrototype;
}

function noPrototypeIterator(
  ctx: IInspectContext,
  value: any,
  recurseTimes: number,
): string {
  let newVal;
  if (isSet(value)) {
    const clazz = clazzWithNullPrototype(Set, 'Set');
    newVal = new clazz(Set_values(value));
  } else if (isMap(value)) {
    const clazz = clazzWithNullPrototype(Map, 'Map');
    newVal = new clazz(Map_entries(value));
  } else if (Array.isArray(value)) {
    const clazz = clazzWithNullPrototype(Array, 'Array');
    newVal = new clazz(value.length);
  } else if (isTypedArray(value)) {
    const constructor = findTypedConstructor(value);
    const clazz = clazzWithNullPrototype(constructor, constructor.name);
    newVal = new clazz(value);
  }
  if (newVal !== undefined) {
    defineProperties(newVal, getOwnPropertyDescriptors(value));
    return formatRaw(ctx, newVal, recurseTimes);
  }
  return (void 0)!;
}

// type InspectFn = (obj: any, opts: IInspectContext) => any;

// function getMessage(self: AssertionError): string {
//   return `${truncate(inspect(self.actual), 128)} ${self.operator} ${truncate(inspect(self.expected), 128)}`;
// }

export function formatNumber(
  fn: (value: string, styleType: keyof typeof styles) => string,
  value: number,
): string {
  return fn(Object_is(value, -0) ? '-0' : `${value}`, 'number');
}

export function formatPrimitive(
  fn: (value: string, styleType: keyof typeof styles) => string,
  value: Primitive,
  ctx: IInspectContext,
): string {
  switch (typeof value) {
    case 'string':
      if (
        ctx.compact !== true &&
        ctx.indentationLvl + value.length > ctx.breakLength &&
        value.length > kMinLineLength
      ) {
        const rawMaxLineLength = ctx.breakLength - ctx.indentationLvl;
        const maxLineLength = Math.max(rawMaxLineLength, kMinLineLength);
        const lines = Math.ceil(value.length / maxLineLength);
        const averageLineLength = Math.ceil(value.length / lines);
        const divisor = Math.max(averageLineLength, kMinLineLength);
        if (readableRegExps[divisor] === void 0) {
          readableRegExps[divisor] = new RegExp(`(.|\\n){1,${divisor}}(\\s|$)|(\\n|.)+?(\\s|$)`, 'gm');
        }

        // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
        const matches = value.match(readableRegExps[divisor])!;
        if (matches.length > 1) {
          const indent = ' '.repeat(ctx.indentationLvl);
          let res = `${fn(escapeAndQuoteString(matches[0]), 'string')} +\n`;
          let i = 1;
          for (; i < matches.length - 1; i++) {
            res += `${indent}  ${fn(escapeAndQuoteString(matches[i]), 'string')} +\n`;
          }
          res += `${indent}  ${fn(escapeAndQuoteString(matches[i]), 'string')}`;
          return res;
        }
      }
      return fn(escapeAndQuoteString(value), 'string');
    case 'number':
      return formatNumber(fn, value);
    case 'boolean':
      return fn(value.toString(), 'boolean');
    case 'undefined':
      return fn('undefined', 'undefined');
    case 'symbol':
      return fn(value.toString(), 'symbol');
  }

  throw new Error(`formatPrimitive only handles non-null primitives. Got: ${Object_toString(value)}`);
}

export function formatError(value: Error): string {
  return value.stack || Error_toString(value);
}

export function formatSpecialArray(
  ctx: IInspectContext,
  value: any[],
  recurseTimes: number,
  maxLength: number,
  output: string[],
  i: number,
): string[] {
  const keys = Object_keys(value);
  let index = i;
  for (; i < keys.length && output.length < maxLength; i++) {
    const key = keys[i];
    const tmp = +key;
    if (tmp > 2 ** 32 - 2) {
      break;
    }
    if (`${index}` !== key) {
      if (!isArrayIndex(key)) {
        break;
      }
      const emptyItems = tmp - index;
      const ending = emptyItems > 1 ? 's' : '';
      const message = `<${emptyItems} empty item${ending}>`;
      output.push(ctx.stylize(message, 'undefined'));
      index = tmp;
      if (output.length === maxLength) {
        break;
      }
    }
    output.push(formatProperty(ctx, value, recurseTimes, key, kArrayType));
    index++;
  }
  const remaining = value.length - index;
  if (output.length !== maxLength) {
    if (remaining > 0) {
      const ending = remaining > 1 ? 's' : '';
      const message = `<${remaining} empty item${ending}>`;
      output.push(ctx.stylize(message, 'undefined'));
    }
  } else if (remaining > 0) {
    output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`);
  }
  return output;
}

export function formatArrayBuffer(
  ctx: IInspectContext,
  value: ArrayBuffer,
): string[] {
  const buffer = new Uint8Array(value);
  let str = join(
    (
      buffer.slice(0, Math.min(ctx.maxArrayLength, buffer.length)) as unknown as number[]
    ).map(val => val.toString(16)),
    ' ',
  );

  const remaining = buffer.length - ctx.maxArrayLength;
  if (remaining > 0) {
    str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
  }
  return [`${ctx.stylize('[Uint8Contents]', 'special')}: <${str}>`];
}

export function formatArray(
  ctx: IInspectContext,
  value: any[],
  recurseTimes: number,
): string[] {
  const valLen = value.length;
  const len = Math.min(Math.max(0, ctx.maxArrayLength), valLen);

  const remaining = valLen - len;
  const output = [];
  for (let i = 0; i < len; i++) {
    if (!hasOwnProperty(value, i)) {
      return formatSpecialArray(ctx, value, recurseTimes, len, output, i);
    }
    output.push(formatProperty(ctx, value, recurseTimes, i, kArrayType));
  }
  if (remaining > 0) {
    output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`);
  }
  return output;
}

export function formatTypedArray(
  ctx: IInspectContext,
  value: TypedArray,
  recurseTimes: number,
): string[] {
  const maxLength = Math.min(Math.max(0, ctx.maxArrayLength), value.length);
  const remaining = value.length - maxLength;
  const output = new Array(maxLength);
  let i = 0;
  for (; i < maxLength; ++i) {
    output[i] = formatNumber(ctx.stylize, value[i]);
  }
  if (remaining > 0) {
    output[i] = `... ${remaining} more item${remaining > 1 ? 's' : ''}`;
  }
  if (ctx.showHidden) {
    ctx.indentationLvl += 2;
    for (const key of typedArrayKeys) {
      const str = formatValue(ctx, value[key], recurseTimes, true);
      output.push(`[${key}]: ${str}`);
    }
    ctx.indentationLvl -= 2;
  }
  return output;
}

export function formatSet(
  ctx: IInspectContext,
  value: Set<any>,
  recurseTimes: number,
): string[] {
  const output = [];
  ctx.indentationLvl += 2;
  for (const v of value) {
    output.push(formatValue(ctx, v, recurseTimes));
  }
  ctx.indentationLvl -= 2;
  if (ctx.showHidden) {
    output.push(`[size]: ${ctx.stylize(value.size.toString(), 'number')}`);
  }
  return output;
}

export function formatMap(
  ctx: IInspectContext,
  value: Map<any, any>,
  recurseTimes: number,
): string[] {
  const output = [];
  ctx.indentationLvl += 2;
  for (const [k, v] of value) {
    output.push(`${formatValue(ctx, k, recurseTimes)} => ${formatValue(ctx, v, recurseTimes)}`);
  }
  ctx.indentationLvl -= 2;
  if (ctx.showHidden) {
    output.push(`[size]: ${ctx.stylize(value.size.toString(), 'number')}`);
  }
  return output;
}

export function formatSetIterInner(
  ctx: IInspectContext,
  recurseTimes: number,
  entries: any[],
  state: number,
): string[] {
  const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
  const maxLength = Math.min(maxArrayLength, entries.length);
  const output: string[] = new Array(maxLength);
  ctx.indentationLvl += 2;
  for (let i = 0; i < maxLength; i++) {
    output[i] = formatValue(ctx, entries[i], recurseTimes);
  }
  ctx.indentationLvl -= 2;
  if (state === kWeak) {
    output.sort();
  }
  const remaining = entries.length - maxLength;
  if (remaining > 0) {
    output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`);
  }
  return output;
}

export function formatMapIterInner(
  ctx: IInspectContext,
  recurseTimes: number,
  entries: any[],
  state: number,
): string[] {
  const maxArrayLength = Math.max(ctx.maxArrayLength, 0);
  const len = entries.length / 2;
  const remaining = len - maxArrayLength;
  const maxLength = Math.min(maxArrayLength, len);
  const output: string[] = new Array(maxLength);
  let start = '';
  let end = '';
  let middle = ' => ';
  let i = 0;
  if (state === kMapEntries) {
    start = '[ ';
    end = ' ]';
    middle = ', ';
  }
  ctx.indentationLvl += 2;
  for (; i < maxLength; i++) {
    const pos = i * 2;
    output[i] = `${start}${formatValue(ctx, entries[pos], recurseTimes)}` +
      `${middle}${formatValue(ctx, entries[pos + 1], recurseTimes)}${end}`;
  }
  ctx.indentationLvl -= 2;
  if (state === kWeak) {
    output.sort();
  }
  if (remaining > 0) {
    output.push(`... ${remaining} more item${remaining > 1 ? 's' : ''}`);
  }
  return output;
}

export function formatWeakCollection(ctx: IInspectContext): string[] {
  return [ctx.stylize('<items unknown>', 'special')];
}

export function formatWeakSet(ctx: IInspectContext, value: any, recurseTimes: number): string[] {
  return formatSetIterInner(ctx, recurseTimes, [], kWeak);
}

export function formatWeakMap(ctx: IInspectContext, value: any, recurseTimes: number): string[] {
  return formatMapIterInner(ctx, recurseTimes, [], kWeak);
}

export function formatIterator(
  ctx: IInspectContext,
  value: Map<any, any> | Set<any>,
  recurseTimes: number,
  braces: [string, string],
): string[] {
  const entries = entriesToArray(value.entries());
  if (value instanceof Map) {
    braces[0] = braces[0].replace(/ Iterator] {$/, ' Entries] {');
    return formatMapIterInner(ctx, recurseTimes, entries, kMapEntries);
  }
  return formatSetIterInner(ctx, recurseTimes, entries, kIterator);
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function formatPromise(ctx: IInspectContext, value: Promise<any>, recurseTimes: number): string[] {
  return ['[object Promise]'];
}

export function formatProperty(
  ctx: IInspectContext,
  value: any,
  recurseTimes: number,
  key: PropertyKey,
  type: number,
): string {
  switch (key) {
    // Aurelia-specific:
    case '$controller':
      return `$controller: { id: ${value.$controller.name} } (omitted for brevity)`;
    case 'overrideContext':
      return 'overrideContext: (omitted for brevity)';
  }

  let name, str;
  let extra = ' ';
  const desc = (
    getOwnPropertyDescriptor(value, key)
    || ({
      value: value[key],
      enumerable: true,
    })
  );
  if (desc.value !== void 0) {
    const diff = (
      type !== kObjectType
      || ctx.compact !== true
    ) ? 2 : 3;

    ctx.indentationLvl += diff;
    str = formatValue(ctx, desc.value, recurseTimes);
    if (diff === 3) {
      const len = ctx.colors
        ? removeColors(str).length
        : str.length;

      if (ctx.breakLength < len) {
        extra = `\n${' '.repeat(ctx.indentationLvl)}`;
      }
    }
    ctx.indentationLvl -= diff;
  } else if (desc.get !== void 0) {
    const label = desc.set !== void 0
      ? 'Getter/Setter'
      : 'Getter';

    const s = ctx.stylize;
    const sp = 'special';
    if (
      ctx.getters
      && (
        ctx.getters === true
        || ctx.getters === 'get' && desc.set === void 0
        || ctx.getters === 'set' && desc.set !== void 0
      )
    ) {
      try {
        const tmp = value[key];
        ctx.indentationLvl += 2;
        if (tmp === null) {
          str = `${s(`[${label}:`, sp)} ${s('null', 'null')}${s(']', sp)}`;
        } else if (typeof tmp === 'object') {
          str = `${s(`[${label}]`, sp)} ${formatValue(ctx, tmp, recurseTimes)}`;
        } else {
          const primitive = formatPrimitive(s, tmp, ctx);
          str = `${s(`[${label}:`, sp)} ${primitive}${s(']', sp)}`;
        }
        ctx.indentationLvl -= 2;
      } catch (err) {
        const message = `<Inspection threw (${(err as Error).message})>`;
        str = `${s(`[${label}:`, sp)} ${message}${s(']', sp)}`;
      }
    } else {
      str = ctx.stylize(`[${label}]`, sp);
    }
  } else if (desc.set !== void 0) {
    str = ctx.stylize('[Setter]', 'special');
  } else {
    str = ctx.stylize('undefined', 'undefined');
  }
  if (type === kArrayType) {
    return str;
  }
  if (isSymbol(key)) {
    const tmp = escapeString(key.toString());
    name = `[${ctx.stylize(tmp, 'symbol')}]`;
  } else if (desc.enumerable === false) {
    name = `[${escapeString(key.toString())}]`;
  } else if (isValidIdentifier(key as string)) {
    name = ctx.stylize((key as string), 'name' as keyof typeof styles);
  } else {
    name = ctx.stylize(escapeAndQuoteString(key as string), 'string');
  }
  return `${name}:${extra}${str}`;
}

export function formatRaw(
  ctx: IInspectContext,
  value: any,
  recurseTimes: number,
  typedArray?: boolean,
): string {
  let keys: PropertyKey[] = (void 0)!;

  const constructor = getConstructorName(value, ctx);
  /* eslint-disable no-fallthrough */
  switch (constructor) {
    // Aurelia-specific:
    // Skip some standard components as their difference will not matter in assertions, but they will
    // generate a lot of noise and slow down the inspection due to their size and property depth
    case 'Container':
    case 'ObserverLocator':
    // Also skip window object as it's not a node instance and therefore not filtered by formatProperty
    case 'Window':
      return ctx.stylize(`${constructor} (omitted for brevity)`, 'special');
    case 'Function':
    // It's likely a constructor, filter out some additional globals that don't matter in inspection
      if (value.name === 'Node') {
        return ctx.stylize('Node constructor (omitted for brevity)', 'special');
      }
  }
  /* eslint-enable no-fallthrough */
  let tag = value[Symbol.toStringTag];
  if (!isString(tag)) {
    tag = '';
  }
  let base = '';
  let formatter: (...args: any[]) => string[] = getEmptyFormatArray;
  let braces: [string, string] = (void 0)!;
  let noIterator = true;
  let i = 0;

  let extrasType = kObjectType;

  // Iterators and the rest are split to reduce checks.
  if (value[Symbol.iterator]) {
    noIterator = false;
    if (Array.isArray(value)) {
      keys = getOwnNonIndexProperties(value, ctx.showHidden);
      // Only set the constructor for non ordinary ("Array [...]") arrays.
      const prefix = getPrefix(constructor, tag, 'Array');
      braces = [`${prefix === 'Array ' ? '' : prefix}[`, ']'];
      if (value.length === 0 && keys.length === 0) {
        return `${braces[0]}]`;
      }
      extrasType = kArrayExtrasType;
      formatter = formatArray;
    } else if (isSet(value)) {
      keys = getKeys(value, ctx.showHidden);
      const prefix = getPrefix(constructor, tag, 'Set');
      if (value.size === 0 && keys.length === 0) {
        return `${prefix}{}`;
      }
      braces = [`${prefix}{`, '}'];
      formatter = formatSet;
    } else if (isMap(value)) {
      keys = getKeys(value, ctx.showHidden);
      const prefix = getPrefix(constructor, tag, 'Map');
      if (value.size === 0 && keys.length === 0) {
        return `${prefix}{}`;
      }
      braces = [`${prefix}{`, '}'];
      formatter = formatMap;
    } else if (isTypedArray(value)) {
      keys = getOwnNonIndexProperties(value, ctx.showHidden);
      const prefix = constructor !== null
        ? getPrefix(constructor, tag)
        : getPrefix(constructor, tag, findTypedConstructor(value).name);
      braces = [`${prefix}[`, ']'];
      if (value.length === 0 && keys.length === 0 && !ctx.showHidden) {
        return `${braces[0]}]`;
      }
      formatter = formatTypedArray;
      extrasType = kArrayExtrasType;
    } else if (isMapIterator(value)) {
      keys = getKeys(value, ctx.showHidden);
      braces = setIteratorBraces('Map', tag);
      formatter = formatIterator;
    } else if (isSetIterator(value)) {
      keys = getKeys(value, ctx.showHidden);
      braces = setIteratorBraces('Set', tag);
      formatter = formatIterator;
    } else {
      noIterator = true;
    }
  }
  if (noIterator) {
    keys = getKeys(value, ctx.showHidden);
    braces = ['{', '}'];
    if (constructor === 'Object') {
      if (isArgumentsObject(value)) {
        braces[0] = '[Arguments] {';
      } else if (tag !== '') {
        braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
      }
      if (keys.length === 0) {
        return `${braces[0]}}`;
      }
    } else if (isFunction(value)) {
      const type = constructor || tag || 'Function';
      let name = `${type}`;
      if (value.name && isString(value.name)) {
        name += `: ${value.name}`;
      }
      if (keys.length === 0) {
        return ctx.stylize(`[${name}]`, 'special');
      }
      base = `[${name}]`;
    } else if (isRegExp(value)) {
      // Make RegExps say that they are RegExps
      base = RegExp_toString(constructor !== null ? value : new RegExp(value));
      const prefix = getPrefix(constructor, tag, 'RegExp');
      if (prefix !== 'RegExp ') {
        base = `${prefix}${base}`;
      }
      if (keys.length === 0 || recurseTimes > ctx.depth && ctx.depth !== null) {
        return ctx.stylize(base, 'regexp');
      }
    } else if (isDate(value)) {
      // Make dates with properties first say the date
      base = Number.isNaN(Date_getTime(value))
        ? Date_toString(value)
        : Date_toISOString(value);
      const prefix = getPrefix(constructor, tag, 'Date');
      if (prefix !== 'Date ') {
        base = `${prefix}${base}`;
      }
      if (keys.length === 0) {
        return ctx.stylize(base, 'date');
      }
    } else if (isError(value)) {
      // Make error with message first say the error.
      base = formatError(value);
      // Wrap the error in brackets in case it has no stack trace.
      const stackStart = base.indexOf('\n    at');
      if (stackStart === -1) {
        base = `[${base}]`;
      }
      // The message and the stack have to be indented as well!
      if (ctx.indentationLvl !== 0) {
        const indentation = ' '.repeat(ctx.indentationLvl);
        base = formatError(value).replace(/\n/g, `\n${indentation}`);
      }
      if (keys.length === 0) {
        return base;
      }

      if (ctx.compact === false && stackStart !== -1) {
        braces[0] += `${base.slice(stackStart)}`;
        base = `[${base.slice(0, stackStart)}]`;
      }
    } else if (isAnyArrayBuffer(value)) {
      // Fast path for ArrayBuffer and SharedArrayBuffer.
      // Can't do the same for DataView because it has a non-primitive
      // .buffer property that we need to recurse for.
      const arrayType = isArrayBuffer(value)
        ? 'ArrayBuffer'
        : 'SharedArrayBuffer';
      const prefix = getPrefix(constructor, tag, arrayType);
      if (typedArray === void 0) {
        formatter = formatArrayBuffer;
      } else if (keys.length === 0) {
        return `${prefix}{ byteLength: ${formatNumber(ctx.stylize, value.byteLength)} }`;
      }
      braces[0] = `${prefix}{`;
      keys.unshift('byteLength');
    } else if (isDataView(value)) {
      braces[0] = `${getPrefix(constructor, tag, 'DataView')}{`;
      // .buffer goes last, it's not a primitive like the others.
      keys.unshift('byteLength', 'byteOffset', 'buffer');
    } else if (isPromise(value)) {
      braces[0] = `${getPrefix(constructor, tag, 'Promise')}{`;
      formatter = formatPromise;
    } else if (isWeakSet(value)) {
      braces[0] = `${getPrefix(constructor, tag, 'WeakSet')}{`;
      formatter = ctx.showHidden ? formatWeakSet : formatWeakCollection;
    } else if (isWeakMap(value)) {
      braces[0] = `${getPrefix(constructor, tag, 'WeakMap')}{`;
      formatter = ctx.showHidden ? formatWeakMap : formatWeakCollection;
    // } else if (isModuleNamespaceObject(value)) {
    //   braces[0] = `[${tag}] {`;
    //   formatter = formatNamespaceObject;
    } else if (isBoxedPrimitive(value)) {
      let type;
      if (isNumberObject(value)) {
        base = `[Number: ${getBoxedValue(Number_valueOf(value), ctx)}]`;
        type = 'number';
      } else if (isStringObject(value)) {
        base = `[String: ${getBoxedValue(String_valueOf(value), ctx)}]`;
        type = 'string';
        // For boxed Strings, we have to remove the 0-n indexed entries,
        // since they just noisy up the output and are redundant
        // Make boxed primitive Strings look like such
        keys = keys.slice(value.length);
      } else if (isBooleanObject(value)) {
        base = `[Boolean: ${getBoxedValue(Boolean_valueOf(value), ctx)}]`;
        type = 'boolean';
      } else {
        base = `[Symbol: ${getBoxedValue(Symbol_valueOf(value), ctx)}]`;
        type = 'symbol';
      }
      if (keys.length === 0) {
        return ctx.stylize(base, type as keyof typeof styles);
      }
    } else {
      // The input prototype got manipulated. Special handle these. We have to
      // rebuild the information so we are able to display everything.
      if (constructor === null) {
        const specialIterator = noPrototypeIterator(ctx, value, recurseTimes);
        if (specialIterator) {
          return specialIterator;
        }
      }
      if (isMapIterator(value)) {
        braces = setIteratorBraces('Map', tag);
        formatter = formatIterator;
      } else if (isSetIterator(value)) {
        braces = setIteratorBraces('Set', tag);
        formatter = formatIterator;
        // Handle other regular objects again.
      } else if (keys.length === 0) {
        // if (isExternal(value)) {
        //   return ctx.stylize('[External]', 'special');
        // }
        return `${getPrefix(constructor, tag, 'Object')}{}`;
      } else {
        braces[0] = `${getPrefix(constructor, tag, 'Object')}{`;
      }
    }
  }

  if (recurseTimes > ctx.depth && ctx.depth !== null) {
    return ctx.stylize(`[${getCtxStyle(constructor, tag)}]`, 'special');
  }
  recurseTimes += 1;

  ctx.seen.push(value);
  ctx.currentDepth = recurseTimes;
  let output;
  const indentationLvl = ctx.indentationLvl;
  try {
    output = formatter(ctx, value, recurseTimes, keys, braces);
    let $key: PropertyKey;
    const isNotNode = PLATFORM?.Node != null && !(value instanceof PLATFORM.Node);
    for (i = 0; i < keys.length; i++) {
      $key = keys[i];
      if (
        // Aurelia-specific:
        // Don't deep inspect html nodes, they are huge, have many irrelevant properties and make the diff unreadable
        (isNotNode || $key === 'textContent' || $key === 'outerHTML')
        // Aurelia-specific:
        // By convention we use $$calls for the CallCollection tracker, never deep inspect that as it's never relevant to the assertion
        && $key !== '$$calls'
      ) {
        output.push(formatProperty(ctx, value, recurseTimes, keys[i] as string | symbol, extrasType));
      }
    }
  } catch (err) {
    return handleMaxCallStackSize(ctx, err as Error, constructor!, tag, indentationLvl);
  }
  ctx.seen.pop();

  if (ctx.sorted) {
    const comparator = ctx.sorted === true ? undefined : ctx.sorted;
    if (extrasType === kObjectType) {
      output.sort(comparator);
    } else if (keys.length > 1) {
      const sorted = output.slice(output.length - keys.length).sort(comparator);
      output.splice(output.length - keys.length, keys.length, ...sorted);
    }
  }

  let combine = false;
  if (isNumber(ctx.compact)) {
    // Memorize the original output length. In case the the output is grouped,
    // prevent lining up the entries on a single line.
    const entries = output.length;
    // Group array elements together if the array contains at least six separate
    // entries.
    if (extrasType === kArrayExtrasType && output.length > 6) {
      output = groupArrayElements(ctx, output);
    }
    // `ctx.currentDepth` is set to the most inner depth of the currently
    // inspected object part while `recurseTimes` is the actual current depth
    // that is inspected.
    //
    // Example:
    //
    // const a = { first: [ 1, 2, 3 ], second: { inner: [ 1, 2, 3 ] } }
    //
    // The deepest depth of `a` is 2 (a.second.inner) and `a.first` has a max
    // depth of 1.
    //
    // Consolidate all entries of the local most inner depth up to
    // `ctx.compact`, as long as the properties are smaller than
    // `ctx.breakLength`.
    if (
      ctx.currentDepth - recurseTimes < ctx.compact
      && entries === output.length
    ) {
      combine = true;
    }
  }

  const res = reduceToSingleString(ctx, output, base, braces, combine);
  const budget = ctx.budget[ctx.indentationLvl] || 0;
  const newLength = budget + res.length;
  ctx.budget[ctx.indentationLvl] = newLength;
  if (newLength > 2 ** 27) {
    ctx.stop = true;
  }
  return res;
}

export function formatValue(
  ctx: IInspectContext,
  value: any,
  recurseTimes: number,
  typedArray?: boolean,
): string {
  if (typeof value !== 'object' && typeof value !== 'function') {
    return formatPrimitive(ctx.stylize, value, ctx);
  }
  if (value === null) {
    return ctx.stylize('null', 'null');
  }

  if (ctx.stop !== void 0) {
    const name = getConstructorName(value, ctx) || (value[Symbol.toStringTag] as string | undefined);
    return ctx.stylize(`[${name || 'Object'}]`, 'special');
  }

  if (ctx.customInspect) {
    const maybeCustom = value[customInspectSymbol];
    if (
      isFunction(maybeCustom)
      && maybeCustom !== inspect
      && !(value.constructor && value.constructor.prototype === value)
    ) {
      const depth = ctx.depth === null ? null : ctx.depth - recurseTimes;
      const ret = maybeCustom.call(value, depth, getUserOptions(ctx));
      if (ret !== value) {
        if (!isString(ret)) {
          return formatValue(ctx, ret, recurseTimes);
        }
        return ret.replace(/\n/g, `\n${' '.repeat(ctx.indentationLvl)}`);
      }
    }
  }

  if (ctx.seen.includes(value)) {
    return ctx.stylize('[Circular]', 'special');
  }

  return formatRaw(ctx, value, recurseTimes, typedArray);
}

export function inspect(value: unknown, opts: Partial<IInspectOptions> = {}): string {
  const ctx = getInspectContext(opts);

  return formatValue(ctx, value, 0);
}

export function inspectValue(val: any): string {
  return inspect(
    val,
    {
      compact: false,
      customInspect: false,
      depth: 100,
      maxArrayLength: Infinity,
      showHidden: false,
      breakLength: Infinity,
      showProxy: false,
      sorted: true,
      getters: true,
    }
  );
}
