# This file contains the common helper functions that we'd like to share among
# the **Lexer**, **Rewriter**, and the **Nodes**. Merge objects, flatten
# arrays, count characters, that sort of thing.

# Peek at the beginning of a given string to see if it matches a sequence.
exports.starts = (string, literal, start) ->
  literal is string.substr start, literal.length

# Peek at the end of a given string to see if it matches a sequence.
exports.ends = (string, literal, back) ->
  len = literal.length
  literal is string.substr string.length - len - (back or 0), len

# Repeat a string `n` times.
exports.repeat = repeat = (str, n) ->
  # Use clever algorithm to have O(log(n)) string concatenation operations.
  res = ''
  while n > 0
    res += str if n & 1
    n >>>= 1
    str += str
  res

# Trim out all falsy values from an array.
exports.compact = (array) ->
  item for item in array when item

# Count the number of occurrences of a string in a string.
exports.count = (string, substr) ->
  num = pos = 0
  return 1/0 unless substr.length
  num++ while pos = 1 + string.indexOf substr, pos
  num

# Merge objects, returning a fresh copy with attributes from both sides.
# Used every time `Base#compile` is called, to allow properties in the
# options hash to propagate down the tree without polluting other branches.
exports.merge = (options, overrides) ->
  extend (extend {}, options), overrides

# Extend a source object with the properties of another object (shallow copy).
extend = exports.extend = (object, properties) ->
  for key, val of properties
    object[key] = val
  object

# Return a flattened version of an array.
# Handy for getting a list of `children` from the nodes.
exports.flatten = flatten = (array) ->
  flattened = []
  for element in array
    if '[object Array]' is Object::toString.call element
      flattened = flattened.concat flatten element
    else
      flattened.push element
  flattened

# Delete a key from an object, returning the value. Useful when a node is
# looking for a particular method in an options hash.
exports.del = (obj, key) ->
  val =  obj[key]
  delete obj[key]
  val

# Typical Array::some
exports.some = Array::some ? (fn) ->
  return true for e in this when fn e
  false

# Helper function for extracting code from Literate CoffeeScript by stripping
# out all non-code blocks, producing a string of CoffeeScript code that can
# be compiled “normally.”
exports.invertLiterate = (code) ->
  out = []
  blankLine = /^\s*$/
  indented = /^[\t ]/
  listItemStart = /// ^
    (?:\t?|\ {0,3})   # Up to one tab, or up to three spaces, or neither;
    (?:
      [\*\-\+] |      # followed by `*`, `-` or `+`;
      [0-9]{1,9}\.    # or by an integer up to 9 digits long, followed by a period;
    )
    [\ \t]            # followed by a space or a tab.
  ///
  insideComment = no
  for line in code.split('\n')
    if blankLine.test(line)
      insideComment = no
      out.push line
    else if insideComment or listItemStart.test(line)
      insideComment = yes
      out.push "# #{line}"
    else if not insideComment and indented.test(line)
      out.push line
    else
      insideComment = yes
      out.push "# #{line}"
  out.join '\n'

# Merge two jison-style location data objects together.
# If `last` is not provided, this will simply return `first`.
buildLocationData = (first, last) ->
  if not last
    first
  else
    first_line: first.first_line
    first_column: first.first_column
    last_line: last.last_line
    last_column: last.last_column
    last_line_exclusive: last.last_line_exclusive
    last_column_exclusive: last.last_column_exclusive
    range: [
      first.range[0]
      last.range[1]
    ]

# Build a list of all comments attached to tokens.
exports.extractAllCommentTokens = (tokens) ->
  allCommentsObj = {}
  for token in tokens when token.comments
    for comment in token.comments
      commentKey = comment.locationData.range[0]
      allCommentsObj[commentKey] = comment
  sortedKeys = Object.keys(allCommentsObj).sort (a, b) -> a - b
  for key in sortedKeys
    allCommentsObj[key]

# Get a lookup hash for a token based on its location data.
# Multiple tokens might have the same location hash, but using exclusive
# location data distinguishes e.g. zero-length generated tokens from
# actual source tokens.
buildLocationHash = (loc) ->
  "#{loc.range[0]}-#{loc.range[1]}"

# Build a dictionary of extra token properties organized by tokens’ locations
# used as lookup hashes.
exports.buildTokenDataDictionary = buildTokenDataDictionary = (tokens) ->
  tokenData = {}
  for token in tokens when token.comments
    tokenHash = buildLocationHash token[2]
    # Multiple tokens might have the same location hash, such as the generated
    # `JS` tokens added at the start or end of the token stream to hold
    # comments that start or end a file.
    tokenData[tokenHash] ?= {}
    if token.comments # `comments` is always an array.
      # For “overlapping” tokens, that is tokens with the same location data
      # and therefore matching `tokenHash`es, merge the comments from both/all
      # tokens together into one array, even if there are duplicate comments;
      # they will get sorted out later.
      (tokenData[tokenHash].comments ?= []).push token.comments...
  tokenData

# This returns a function which takes an object as a parameter, and if that
# object is an AST node, updates that object's locationData.
# The object is returned either way.
exports.addDataToNode = (parserState, firstLocationData, firstValue, lastLocationData, lastValue, forceUpdateLocation = yes) ->
  (obj) ->
    # Add location data.
    locationData = buildLocationData(firstValue?.locationData ? firstLocationData, lastValue?.locationData ? lastLocationData)
    if obj?.updateLocationDataIfMissing? and firstLocationData?
      obj.updateLocationDataIfMissing locationData, forceUpdateLocation
    else
      obj.locationData = locationData

    # Add comments, building the dictionary of token data if it hasn’t been
    # built yet.
    parserState.tokenData ?= buildTokenDataDictionary parserState.parser.tokens
    if obj.locationData?
      objHash = buildLocationHash obj.locationData
      if parserState.tokenData[objHash]?.comments?
        attachCommentsToNode parserState.tokenData[objHash].comments, obj
    obj

exports.attachCommentsToNode = attachCommentsToNode = (comments, node) ->
  return if not comments? or comments.length is 0
  node.comments ?= []
  node.comments.push comments...

# Convert jison location data to a string.
# `obj` can be a token, or a locationData.
exports.locationDataToString = (obj) ->
  if ("2" of obj) and ("first_line" of obj[2]) then locationData = obj[2]
  else if "first_line" of obj then locationData = obj

  if locationData
    "#{locationData.first_line + 1}:#{locationData.first_column + 1}-" +
    "#{locationData.last_line + 1}:#{locationData.last_column + 1}"
  else
    "No location data"

# Generate a unique anonymous file name so we can distinguish source map cache
# entries for any number of anonymous scripts.
exports.anonymousFileName = do ->
  n = 0
  ->
    "<anonymous-#{n++}>"

# A `.coffee.md` compatible version of `basename`, that returns the file sans-extension.
exports.baseFileName = (file, stripExt = no, useWinPathSep = no) ->
  pathSep = if useWinPathSep then /\\|\// else /\//
  parts = file.split(pathSep)
  file = parts[parts.length - 1]
  return file unless stripExt and file.indexOf('.') >= 0
  parts = file.split('.')
  parts.pop()
  parts.pop() if parts[parts.length - 1] is 'coffee' and parts.length > 1
  parts.join('.')

# Determine if a filename represents a CoffeeScript file.
exports.isCoffee = (file) -> /\.((lit)?coffee|coffee\.md)$/.test file

# Determine if a filename represents a Literate CoffeeScript file.
exports.isLiterate = (file) -> /\.(litcoffee|coffee\.md)$/.test file

# Throws a SyntaxError from a given location.
# The error's `toString` will return an error message following the "standard"
# format `<filename>:<line>:<col>: <message>` plus the line with the error and a
# marker showing where the error is.
exports.throwSyntaxError = (message, location) ->
  error = new SyntaxError message
  error.location = location
  error.toString = syntaxErrorToString

  # Instead of showing the compiler's stacktrace, show our custom error message
  # (this is useful when the error bubbles up in Node.js applications that
  # compile CoffeeScript for example).
  error.stack = error.toString()

  throw error

# Update a compiler SyntaxError with source code information if it didn't have
# it already.
exports.updateSyntaxError = (error, code, filename) ->
  # Avoid screwing up the `stack` property of other errors (i.e. possible bugs).
  if error.toString is syntaxErrorToString
    error.code or= code
    error.filename or= filename
    error.stack = error.toString()
  error

syntaxErrorToString = ->
  return Error::toString.call @ unless @code and @location

  {first_line, first_column, last_line, last_column} = @location
  last_line ?= first_line
  last_column ?= first_column

  if @filename?.startsWith '<anonymous'
    filename = '[stdin]'
  else
    filename = @filename or '[stdin]'

  codeLine = @code.split('\n')[first_line]
  start    = first_column
  # Show only the first line on multi-line errors.
  end      = if first_line is last_line then last_column + 1 else codeLine.length
  marker   = codeLine[...start].replace(/[^\s]/g, ' ') + repeat('^', end - start)

  # Check to see if we're running on a color-enabled TTY.
  if process?
    colorsEnabled = process.stdout?.isTTY and not process.env?.NODE_DISABLE_COLORS

  if @colorful ? colorsEnabled
    colorize = (str) -> "\x1B[1;31m#{str}\x1B[0m"
    codeLine = codeLine[...start] + colorize(codeLine[start...end]) + codeLine[end..]
    marker   = colorize marker

  """
    #{filename}:#{first_line + 1}:#{first_column + 1}: error: #{@message}
    #{codeLine}
    #{marker}
  """

exports.nameWhitespaceCharacter = (string) ->
  switch string
    when ' ' then 'space'
    when '\n' then 'newline'
    when '\r' then 'carriage return'
    when '\t' then 'tab'
    else string

exports.parseNumber = (string) ->
  return NaN unless string?

  base = switch string.charAt 1
    when 'b' then 2
    when 'o' then 8
    when 'x' then 16
    else null

  if base?
    parseInt string[2..].replace(/_/g, ''), base
  else
    parseFloat string.replace(/_/g, '')

exports.isFunction = (obj) -> Object::toString.call(obj) is '[object Function]'
exports.isNumber = isNumber = (obj) -> Object::toString.call(obj) is '[object Number]'
exports.isString = isString = (obj) -> Object::toString.call(obj) is '[object String]'
exports.isBoolean = isBoolean = (obj) -> obj is yes or obj is no or Object::toString.call(obj) is '[object Boolean]'
exports.isPlainObject = (obj) -> typeof obj is 'object' and !!obj and not Array.isArray(obj) and not isNumber(obj) and not isString(obj) and not isBoolean(obj)

unicodeCodePointToUnicodeEscapes = (codePoint) ->
  toUnicodeEscape = (val) ->
    str = val.toString 16
    "\\u#{repeat '0', 4 - str.length}#{str}"
  return toUnicodeEscape(codePoint) if codePoint < 0x10000
  # surrogate pair
  high = Math.floor((codePoint - 0x10000) / 0x400) + 0xD800
  low = (codePoint - 0x10000) % 0x400 + 0xDC00
  "#{toUnicodeEscape(high)}#{toUnicodeEscape(low)}"

# Replace `\u{...}` with `\uxxxx[\uxxxx]` in regexes without `u` flag
exports.replaceUnicodeCodePointEscapes = (str, {flags, error, delimiter = ''} = {}) ->
  shouldReplace = flags? and 'u' not in flags
  str.replace UNICODE_CODE_POINT_ESCAPE, (match, escapedBackslash, codePointHex, offset) ->
    return escapedBackslash if escapedBackslash

    codePointDecimal = parseInt codePointHex, 16
    if codePointDecimal > 0x10ffff
      error "unicode code point escapes greater than \\u{10ffff} are not allowed",
        offset: offset + delimiter.length
        length: codePointHex.length + 4
    return match unless shouldReplace

    unicodeCodePointToUnicodeEscapes codePointDecimal

UNICODE_CODE_POINT_ESCAPE = ///
  ( \\\\ )        # Make sure the escape isn’t escaped.
  |
  \\u\{ ( [\da-fA-F]+ ) \}
///g
