/**
 * Based on Kendo UI Core expression code <https://github.com/telerik/kendo-ui-core#license-information>
 */
'use strict'

function Cache(maxSize) {
  this._maxSize = maxSize
  this.clear()
}
Cache.prototype.clear = function() {
  this._size = 0
  this._values = {}
}
Cache.prototype.get = function(key) {
  return this._values[key]
}
Cache.prototype.set = function(key, value) {
  this._size >= this._maxSize && this.clear()
  if (!this._values.hasOwnProperty(key)) {
    this._size++
  }
  return this._values[key] = value
}

var SPLIT_REGEX = /[^.^\]^[]+|(?=\[\]|\.\.)/g,
  DIGIT_REGEX = /^\d+$/,
  LEAD_DIGIT_REGEX = /^\d/,
  SPEC_CHAR_REGEX = /[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/g,
  CLEAN_QUOTES_REGEX = /^\s*(['"]?)(.*?)(\1)\s*$/,
  MAX_CACHE_SIZE = 512

var contentSecurityPolicy = false,
  pathCache = new Cache(MAX_CACHE_SIZE),
  setCache = new Cache(MAX_CACHE_SIZE),
  getCache = new Cache(MAX_CACHE_SIZE)

try {
  new Function('')
} catch (error) {
  contentSecurityPolicy = true
}

module.exports = {
  Cache: Cache,

  expr: expr,

  split: split,

  normalizePath: normalizePath,

  setter: contentSecurityPolicy
    ? function(path) {
      var parts = normalizePath(path)
      return function(data, value) {
        return setterFallback(parts, data, value)
      }
    }
    : function(path) {
      return setCache.get(path) || setCache.set(
        path,
        new Function(
          'data, value',
          expr(path, 'data') + ' = value'
        )
      )
    },

  getter: contentSecurityPolicy
    ? function(path, safe) {
      var parts = normalizePath(path)
      return function(data) {
        return getterFallback(parts, safe, data)
      }
    }
    : function(path, safe) {
      var key = path + '_' + safe
      return getCache.get(key) || getCache.set(
        key,
        new Function('data', 'return ' + expr(path, safe, 'data'))
      )
    },

  join: function(segments) {
    return segments.reduce(function(path, part) {
      return (
        path +
        (isQuoted(part) || DIGIT_REGEX.test(part)
          ? '[' + part + ']'
          : (path ? '.' : '') + part)
      )
    }, '')
  },

  forEach: function(path, cb, thisArg) {
    forEach(split(path), cb, thisArg)
  }
}

function setterFallback(parts, data, value) {
  var index = 0,
    len = parts.length
  while (index < len - 1) {
    data = data[parts[index++]]
  }
  data[parts[index]] = value
}

function getterFallback(parts, safe, data) {
  var index = 0,
    len = parts.length
  while (index < len) {
    if (data != null || !safe) {
      data = data[parts[index++]]
    } else {
      return
    }
  }
  return data
}

function normalizePath(path) {
  return pathCache.get(path) || pathCache.set(
    path,
    split(path).map(function(part) {
      return part.replace(CLEAN_QUOTES_REGEX, '$2')
    })
  )
}

function split(path) {
  return path.match(SPLIT_REGEX)
}

function expr(expression, safe, param) {
  expression = expression || ''

  if (typeof safe === 'string') {
    param = safe
    safe = false
  }

  param = param || 'data'

  if (expression && expression.charAt(0) !== '[') expression = '.' + expression

  return safe ? makeSafe(expression, param) : param + expression
}

function forEach(parts, iter, thisArg) {
  var len = parts.length,
    part,
    idx,
    isArray,
    isBracket

  for (idx = 0; idx < len; idx++) {
    part = parts[idx]

    if (part) {
      if (shouldBeQuoted(part)) {
        part = '"' + part + '"'
      }

      isBracket = isQuoted(part)
      isArray = !isBracket && /^\d+$/.test(part)

      iter.call(thisArg, part, isBracket, isArray, idx, parts)
    }
  }
}

function isQuoted(str) {
  return (
    typeof str === 'string' && str && ["'", '"'].indexOf(str.charAt(0)) !== -1
  )
}

function makeSafe(path, param) {
  var result = param,
    parts = split(path),
    isLast

  forEach(parts, function(part, isBracket, isArray, idx, parts) {
    isLast = idx === parts.length - 1

    part = isBracket || isArray ? '[' + part + ']' : '.' + part

    result += part + (!isLast ? ' || {})' : ')')
  })

  return new Array(parts.length + 1).join('(') + result
}

function hasLeadingNumber(part) {
  return part.match(LEAD_DIGIT_REGEX) && !part.match(DIGIT_REGEX)
}

function hasSpecialChars(part) {
  return SPEC_CHAR_REGEX.test(part)
}

function shouldBeQuoted(part) {
  return !isQuoted(part) && (hasLeadingNumber(part) || hasSpecialChars(part))
}