text: 'use strict'
  
  const clone = require('rfdc')({ circles: true })
  const dateformat = require('dateformat')
  const stringifySafe = require('fast-safe-stringify')
  const defaultColorizer = require('./colors')()
  const {
    DATE_FORMAT,
    ERROR_LIKE_KEYS,
    MESSAGE_KEY,
    LEVEL_KEY,
    LEVEL_LABEL,
    TIMESTAMP_KEY,
    LOGGER_KEYS,
    LEVELS
  } = require('./constants')
  
  module.exports = {
    isObject,
    prettifyErrorLog,
    prettifyLevel,
    prettifyMessage,
    prettifyMetadata,
    prettifyObject,
    prettifyTime,
    filterLog
  }
  
  module.exports.internals = {
    formatTime,
    joinLinesWithIndentation,
    prettifyError,
    deleteLogProperty,
    splitIgnoreKey,
    createDate,
    isValidDate
  }
  
  /**
   * Converts a given `epoch` to a desired display format.
   *
   * @param {number|string} epoch The time to convert. May be any value that is
   * valid for `new Date()`.
   * @param {boolean|string} [translateTime=false] When `false`, the given `epoch`
   * will simply be returned. When `true`, the given `epoch` will be converted
   * to a string at UTC using the `DATE_FORMAT` constant. If `translateTime` is
   * a string, the following rules are available:
   *
   * - `<format string>`: The string is a literal format string. This format
   * string will be used to interpret the `epoch` and return a display string
   * at UTC.
   * - `SYS:STANDARD`: The returned display string will follow the `DATE_FORMAT`
   * constant at the system's local timezone.
   * - `SYS:<format string>`: The returned display string will follow the given
   * `<format string>` at the system's local timezone.
   * - `UTC:<format string>`: The returned display string will follow the given
   * `<format string>` at UTC.
   *
   * @returns {number|string} The formatted time.
   */
  function formatTime (epoch, translateTime = false) {
    if (translateTime === false) {
      return epoch
    }
  
    const instant = createDate(epoch)
  
    // If the Date is invalid, do not attempt to format
    if (!isValidDate(instant)) {
      return epoch
    }
  
    if (translateTime === true) {
      return dateformat(instant, 'UTC:' + DATE_FORMAT)
    }
  
    const upperFormat = translateTime.toUpperCase()
    if (upperFormat === 'SYS:STANDARD') {
      return dateformat(instant, DATE_FORMAT)
    }
  
    const prefix = upperFormat.substr(0, 4)
    if (prefix === 'SYS:' || prefix === 'UTC:') {
      if (prefix === 'UTC:') {
        return dateformat(instant, translateTime)
      }
      return dateformat(instant, translateTime.slice(4))
    }
  
    return dateformat(instant, `UTC:${translateTime}`)
  }
  
  /**
   * Constructs a JS Date from a number or string. Accepts any single number
   * or single string argument that is valid for the Date() constructor,
   * or an epoch as a string.
   *
   * @param {string|number} epoch The representation of the Date.
   *
   * @returns {Date} The constructed Date.
   */
  function createDate (epoch) {
    // If epoch is already a valid argument, return the valid Date
    let date = new Date(epoch)
    if (isValidDate(date)) {
      return date
    }
  
    // Convert to a number to permit epoch as a string
    date = new Date(+epoch)
    return date
  }
  
  /**
   * Checks if the argument is a JS Date and not 'Invalid Date'.
   *
   * @param {Date} date The date to check.
   *
   * @returns {boolean} true if the argument is a JS Date and not 'Invalid Date'.
   */
  function isValidDate (date) {
    return date instanceof Date && !Number.isNaN(date.getTime())
  }
  
  function isObject (input) {
    return Object.prototype.toString.apply(input) === '[object Object]'
  }
  
  /**
   * Given a string with line separators, either `\r\n` or `\n`, add indentation
   * to all lines subsequent to the first line and rejoin the lines using an
   * end of line sequence.
   *
   * @param {object} input
   * @param {string} input.input The string to split and reformat.
   * @param {string} [input.ident] The indentation string. Default: `    ` (4 spaces).
   * @param {string} [input.eol] The end of line sequence to use when rejoining
   * the lines. Default: `'\n'`.
   *
   * @returns {string} A string with lines subsequent to the first indented
   * with the given indentation sequence.
   */
  function joinLinesWithIndentation ({ input, ident = '    ', eol = '\n' }) {
    const lines = input.split(/\r?\n/)
    for (let i = 1; i < lines.length; i += 1) {
      lines[i] = ident + lines[i]
    }
    return lines.join(eol)
  }
  
  /**
   * Given a log object that has a `type: 'Error'` key, prettify the object and
   * return the result. In other
   *
   * @param {object} input
   * @param {object} input.log The error log to prettify.
   * @param {string} [input.messageKey] The name of the key that contains a
   * general log message. This is not the error's message property but the logger
   * messsage property. Default: `MESSAGE_KEY` constant.
   * @param {string} [input.ident] The sequence to use for indentation. Default: `'    '`.
   * @param {string} [input.eol] The sequence to use for EOL. Default: `'\n'`.
   * @param {string[]} [input.errorLikeKeys] A set of keys that should be considered
   * to have error objects as values. Default: `ERROR_LIKE_KEYS` constant.
   * @param {string[]} [input.errorProperties] A set of specific error object
   * properties, that are not the value of `messageKey`, `type`, or `stack`, to
   * include in the prettified result. The first entry in the list may be `'*'`
   * to indicate that all sibiling properties should be prettified. Default: `[]`.
   *
   * @returns {string} A sring that represents the prettified error log.
   */
  function prettifyErrorLog ({
    log,
    messageKey = MESSAGE_KEY,
    ident = '    ',
    eol = '\n',
    errorLikeKeys = ERROR_LIKE_KEYS,
    errorProperties = []
  }) {
    const stack = log.stack
    const joinedLines = joinLinesWithIndentation({ input: stack, ident, eol })
    let result = `${ident}${joinedLines}${eol}`
  
    if (errorProperties.length > 0) {
      const excludeProperties = LOGGER_KEYS.concat(messageKey, 'type', 'stack')
      let propertiesToPrint
      if (errorProperties[0] === '*') {
        // Print all sibling properties except for the standard exclusions.
        propertiesToPrint = Object.keys(log).filter(k => excludeProperties.includes(k) === false)
      } else {
        // Print only specified properties unless the property is a standard exclusion.
        propertiesToPrint = errorProperties.filter(k => excludeProperties.includes(k) === false)
      }
  
      for (let i = 0; i < propertiesToPrint.length; i += 1) {
        const key = propertiesToPrint[i]
        if (key in log === false) continue
        if (isObject(log[key])) {
          // The nested object may have "logger" type keys but since they are not
          // at the root level of the object being processed, we want to print them.
          // Thus, we invoke with `excludeLoggerKeys: false`.
          const prettifiedObject = prettifyObject({ input: log[key], errorLikeKeys, excludeLoggerKeys: false, eol, ident: ident + ident })
          result = `${result}${ident}${key}: {${eol}${prettifiedObject}${ident}}${eol}`
          continue
        }
        result = `${result}${ident}${key}: ${log[key]}${eol}`
      }
    }
  
    return result
  }
  
  /**
   * Checks if the passed in log has a `level` value and returns a prettified
   * string for that level if so.
   *
   * @param {object} input
   * @param {object} input.log The log object.
   * @param {function} [input.colorizer] A colorizer function that accepts a level
   * value and returns a colorized string. Default: a no-op colorizer.
   * @param {string} [levelKey='level'] The key to find the level under.
   * @param {function} [input.prettifier] A user-supplied formatter to be called instead of colorizer.
   *
   * @returns {undefined|string} If `log` does not have a `level` property then
   * `undefined` will be returned. Otherwise, a string from the specified
   * `colorizer` is returned.
   */
  function prettifyLevel ({ log, colorizer = defaultColorizer, levelKey = LEVEL_KEY, prettifier }) {
    if (levelKey in log === false) return undefined
    const output = log[levelKey]
    return prettifier ? prettifier(output) : colorizer(output)
  }
  
  /**
   * Prettifies a message string if the given `log` has a message property.
   *
   * @param {object} input
   * @param {object} input.log The log object with the message to colorize.
   * @param {string} [input.messageKey='msg'] The property of the `log` that is the
   * message to be prettified.
   * @param {string|function} [input.messageFormat=undefined] A format string or function that defines how the
   *  logged message should be formatted, e.g. `'{level} - {pid}'`.
   * @param {function} [input.colorizer] A colorizer function that has a
   * `.message(str)` method attached to it. This function should return a colorized
   * string which will be the "prettified" message. Default: a no-op colorizer.
   *
   * @returns {undefined|string} If the message key is not found, or the message
   * key is not a string, then `undefined` will be returned. Otherwise, a string
   * that is the prettified message.
   */
  function prettifyMessage ({ log, messageFormat, messageKey = MESSAGE_KEY, colorizer = defaultColorizer, levelLabel = LEVEL_LABEL }) {
    if (messageFormat && typeof messageFormat === 'string') {
      const message = String(messageFormat).replace(/{([^{}]+)}/g, function (match, p1) {
        // return log level as string instead of int
        if (p1 === levelLabel && log[LEVEL_KEY]) {
          return LEVELS[log[LEVEL_KEY]]
        }
        // Parse nested key access, e.g. `{keyA.subKeyB}`.
        return p1.split('.').reduce(function (prev, curr) {
          if (prev && prev[curr]) {
            return prev[curr]
          }
          return ''
        }, log)
      })
      return colorizer.message(message)
    }
    if (messageFormat && typeof messageFormat === 'function') {
      const msg = messageFormat(log, messageKey, levelLabel)
      return colorizer.message(msg)
    }
    if (messageKey in log === false) return undefined
    if (typeof log[messageKey] !== 'string') return undefined
    return colorizer.message(log[messageKey])
  }
  
  /**
   * Prettifies metadata that is usually present in a Pino log line. It looks for
   * fields `name`, `pid`, `hostname`, and `caller` and returns a formatted string using
   * the fields it finds.
   *
   * @param {object} input
   * @param {object} input.log The log that may or may not contain metadata to
   * be prettified.
   *
   * @returns {undefined|string} If no metadata is found then `undefined` is
   * returned. Otherwise, a string of prettified metadata is returned.
   */
  function prettifyMetadata ({ log }) {
    let line = ''
  
    if (log.name || log.pid || log.hostname) {
      line += '('
  
      if (log.name) {
        line += log.name
      }
  
      if (log.name && log.pid) {
        line += '/' + log.pid
      } else if (log.pid) {
        line += log.pid
      }
  
      if (log.hostname) {
        // If `pid` and `name` were in the ignore keys list then we don't need
        // the leading space.
        line += `${line === '(' ? 'on' : ' on'} ${log.hostname}`
      }
  
      line += ')'
    }
  
    if (log.caller) {
      line += `${line === '' ? '' : ' '}<${log.caller}>`
    }
  
    if (line === '') {
      return undefined
    } else {
      return line
    }
  }
  
  /**
   * Prettifies a standard object. Special care is taken when processing the object
   * to handle child objects that are attached to keys known to contain error
   * objects.
   *
   * @param {object} input
   * @param {object} input.input The object to prettify.
   * @param {string} [input.ident] The identation sequence to use. Default: `'    '`.
   * @param {string} [input.eol] The EOL sequence to use. Default: `'\n'`.
   * @param {string[]} [input.skipKeys] A set of object keys to exclude from the
   * prettified result. Default: `[]`.
   * @param {Object<string, function>} [input.customPrettifiers] Dictionary of
   * custom prettifiers. Default: `{}`.
   * @param {string[]} [input.errorLikeKeys] A set of object keys that contain
   * error objects. Default: `ERROR_LIKE_KEYS` constant.
   * @param {boolean} [input.excludeLoggerKeys] Indicates if known logger specific
   * keys should be excluded from prettification. Default: `true`.
   * @param {boolean} [input.singleLine] Should non-error keys all be formatted
   * on a single line? This does NOT apply to errors, which will still be
   * multi-line. Default: `false`
   *
   * @returns {string} The prettified string. This can be as little as `''` if
   * there was nothing to prettify.
   */
  function prettifyObject ({
    input,
    ident = '    ',
    eol = '\n',
    skipKeys = [],
    customPrettifiers = {},
    errorLikeKeys = ERROR_LIKE_KEYS,
    excludeLoggerKeys = true,
    singleLine = false,
    colorizer = defaultColorizer
  }) {
    const keysToIgnore = [].concat(skipKeys)
  
    if (excludeLoggerKeys === true) Array.prototype.push.apply(keysToIgnore, LOGGER_KEYS)
  
    let result = ''
  
    // Split object keys into two categories: error and non-error
    const { plain, errors } = Object.entries(input).reduce(({ plain, errors }, [k, v]) => {
      if (keysToIgnore.includes(k) === false) {
        // Pre-apply custom prettifiers, because all 3 cases below will need this
        const pretty = typeof customPrettifiers[k] === 'function'
          ? customPrettifiers[k](v, k, input)
          : v
        if (errorLikeKeys.includes(k)) {
          errors[k] = pretty
        } else {
          plain[k] = pretty
        }
      }
      return { plain, errors }
    }, { plain: {}, errors: {} })
  
    if (singleLine) {
      // Stringify the entire object as a single JSON line
      if (Object.keys(plain).length > 0) {
        result += colorizer.greyMessage(stringifySafe(plain))
      }
      result += eol
    } else {
      // Put each object entry on its own line
      Object.entries(plain).forEach(([keyName, keyValue]) => {
        // custom prettifiers are already applied above, so we can skip it now
        const lines = typeof customPrettifiers[keyName] === 'function'
          ? keyValue
          : stringifySafe(keyValue, null, 2)
  
        if (lines === undefined) return
  
        const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
        result += `${ident}${keyName}: ${joinedLines}${eol}`
      })
    }
  
    // Errors
    Object.entries(errors).forEach(([keyName, keyValue]) => {
      // custom prettifiers are already applied above, so we can skip it now
      const lines = typeof customPrettifiers[keyName] === 'function'
        ? keyValue
        : stringifySafe(keyValue, null, 2)
  
      if (lines === undefined) return
  
      result += prettifyError({ keyName, lines, eol, ident })
    })
  
    return result
  }
  
  /**
   * Prettifies a timestamp if the given `log` has either `time`, `timestamp` or custom specified timestamp
   * property.
   *
   * @param {object} input
   * @param {object} input.log The log object with the timestamp to be prettified.
   * @param {string} [input.timestampKey='time'] The log property that should be used to resolve timestamp value
   * @param {boolean|string} [input.translateFormat=undefined] When `true` the
   * timestamp will be prettified into a string at UTC using the default
   * `DATE_FORMAT`. If a string, then `translateFormat` will be used as the format
   * string to determine the output; see the `formatTime` function for details.
   * @param {function} [input.prettifier] A user-supplied formatter for altering output.
   *
   * @returns {undefined|string} If a timestamp property cannot be found then
   * `undefined` is returned. Otherwise, the prettified time is returned as a
   * string.
   */
  function prettifyTime ({ log, timestampKey = TIMESTAMP_KEY, translateFormat = undefined, prettifier }) {
    let time = null
  
    if (timestampKey in log) {
      time = log[timestampKey]
    } else if ('timestamp' in log) {
      time = log.timestamp
    }
  
    if (time === null) return undefined
    const output = translateFormat ? formatTime(time, translateFormat) : time
  
    return prettifier ? prettifier(output) : `[${output}]`
  }
  
  /**
   * Prettifies an error string into a multi-line format.
   * @param {object} input
   * @param {string} input.keyName The key assigned to this error in the log object
   * @param {string} input.lines The STRINGIFIED error. If the error field has a
   *  custom prettifier, that should be pre-applied as well
   * @param {string} input.ident The indentation sequence to use
   * @param {string} input.eol The EOL sequence to use
   */
  function prettifyError ({ keyName, lines, eol, ident }) {
    let result = ''
    const joinedLines = joinLinesWithIndentation({ input: lines, ident, eol })
    const splitLines = `${ident}${keyName}: ${joinedLines}${eol}`.split(eol)
  
    for (let j = 0; j < splitLines.length; j += 1) {
      if (j !== 0) result += eol
  
      const line = splitLines[j]
      if (/^\s*"stack"/.test(line)) {
        const matches = /^(\s*"stack":)\s*(".*"),?$/.exec(line)
        /* istanbul ignore else */
        if (matches && matches.length === 3) {
          const indentSize = /^\s*/.exec(line)[0].length + 4
          const indentation = ' '.repeat(indentSize)
          const stackMessage = matches[2]
          result += matches[1] + eol + indentation + JSON.parse(stackMessage).replace(/\n/g, eol + indentation)
        } else {
          result += line
        }
      } else {
        result += line
      }
    }
  
    return result
  }
  
  /**
   * Splits the input key delimited by a dot character but not when it is preceded
   * by a backslash.
   *
   * @param {string} key A string identifying the property.
   *
   * @returns {string[]} Returns a list of string containing each delimited property.
   * e.g. `'prop2\.domain\.corp.prop2'` should return [ 'prop2.domain.com', 'prop2' ]
   */
  function splitIgnoreKey (key) {
    const result = []
    let backslash = false
    let segment = ''
  
    for (let i = 0; i < key.length; i++) {
      const c = key.charAt(i)
  
      if (c === '\\') {
        backslash = true
        continue
      }
  
      if (backslash) {
        backslash = false
        segment += c
        continue
      }
  
      /* Non-escaped dot, push to result */
      if (c === '.') {
        result.push(segment)
        segment = ''
        continue
      }
  
      segment += c
    }
  
    /* Push last entry to result */
    if (segment.length) {
      result.push(segment)
    }
  
    return result
  }
  
  /**
   * Deletes a specified property from a log object if it exists.
   * This function mutates the passed in `log` object.
   *
   * @param {object} log The log object to be modified.
   * @param {string} property A string identifying the property to be deleted from
   * the log object. Accepts nested properties delimited by a `.`
   * Delimiter can be escaped to preserve property names that contain the delimiter.
   * e.g. `'prop1.prop2'` or `'prop2\.domain\.corp.prop2'`
   */
  function deleteLogProperty (log, property) {
    const props = splitIgnoreKey(property)
    const propToDelete = props.pop()
  
    props.forEach((prop) => {
      if (!Object.prototype.hasOwnProperty.call(log, prop)) {
        return
      }
      log = log[prop]
    })
  
    delete log[propToDelete]
  }
  
  /**
   * Filter a log object by removing any ignored keys.
   *
   * @param {object} log The log object to be modified.
   * @param {string} ignoreKeys An array of strings identifying the properties to be removed.
   *
   * @returns {object} A new `log` object instance that does not include the ignored keys.
   */
  function filterLog (log, ignoreKeys) {
    const logCopy = clone(log)
    ignoreKeys.forEach((ignoreKey) => {
      deleteLogProperty(logCopy, ignoreKey)
    })
    return logCopy
  }
  