var Big = require('big.js'),
    db = require('../db'),
    conditionParser = require('../db/conditionParser'),
    projectionParser = require('../db/projectionParser'),
    updateParser = require('../db/updateParser')

exports.checkTypes = checkTypes
exports.checkValidations = checkValidations
exports.toLowerFirst = toLowerFirst
exports.findDuplicate = findDuplicate
exports.validateAttributeValue = validateAttributeValue
exports.validateConditions = validateConditions
exports.validateAttributeConditions = validateAttributeConditions
exports.validateExpressionParams = validateExpressionParams
exports.validateExpressions = validateExpressions
exports.convertKeyCondition = convertKeyCondition

function checkTypes(data, types) {
  var key
  for (key in data) {
    // TODO: deal with nulls
    if (!types[key] || data[key] == null)
      delete data[key]
  }

  return Object.keys(types).reduce(function(newData, key) {
    var val = checkType(data[key], types[key])
    if (val != null) newData[key] = val
    return newData
  }, {})

  function typeError(msg) {
    var err = new Error(msg)
    err.statusCode = 400
    err.body = {
      __type: 'com.amazon.coral.service#SerializationException',
      Message: msg,
    }
    return err
  }

  function checkType(val, type) {
    if (val == null) return null
    var children = type.children
    if (typeof children == 'string') children = {type: children}
    if (type.type) type = type.type
    var subtypeMatch = type.match(/(.+?)<(.+)>$/), subtype
    if (subtypeMatch != null) {
      type = subtypeMatch[1]
      subtype = subtypeMatch[2]
    }

    if (type == 'AttrStruct') {
      return checkType(val, {
        type: subtype + '<AttributeValue>',
        children: {
          S: 'String',
          B: 'Blob',
          N: 'String',
          BOOL: 'Boolean',
          NULL: 'Boolean',
          BS: {
            type: 'List',
            children: 'Blob',
          },
          NS: {
            type: 'List',
            children: 'String',
          },
          SS: {
            type: 'List',
            children: 'String',
          },
          L: {
            type: 'List',
            children: 'AttrStruct<ValueStruct>',
          },
          M: {
            type: 'Map<AttributeValue>',
            children: 'AttrStruct<ValueStruct>',
          },
        },
      })
    }

    switch (type) {
      case 'Boolean':
        switch (typeof val) {
          case 'number':
            throw typeError((val % 1 === 0 ? 'NUMBER_VALUE' : 'DECIMAL_VALUE') + ' cannot be converted to ' + type)
          case 'string':
            // 'true'/'false'/'1'/'0'/'no'/'yes' seem to convert fine
            val = val.toUpperCase()
            if (~['TRUE', '1', 'YES'].indexOf(val)) {
              val = true
            } else if (~['FALSE', '0', 'NO'].indexOf(val)) {
              val = false
            } else {
              throw typeError('Unexpected token received from parser')
            }
            break
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type)
            throw typeError('Start of structure or map found where not expected')
        }
        return val
      case 'Short':
      case 'Integer':
      case 'Long':
      case 'Double':
        switch (typeof val) {
          case 'boolean':
            throw typeError((val ? 'TRUE_VALUE' : 'FALSE_VALUE') + ' cannot be converted to ' + type)
          case 'number':
            if (type != 'Double') val = Math.floor(val)
            break
          case 'string':
            throw typeError('STRING_VALUE cannot be converted to ' + type)
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type)
            throw typeError('Start of structure or map found where not expected')
        }
        return val
      case 'String':
        switch (typeof val) {
          case 'boolean':
            throw typeError((val ? 'TRUE_VALUE' : 'FALSE_VALUE') + ' cannot be converted to ' + type)
          case 'number':
            throw typeError((val % 1 === 0 ? 'NUMBER_VALUE' : 'DECIMAL_VALUE') + ' cannot be converted to ' + type)
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type)
            throw typeError('Start of structure or map found where not expected')
        }
        return val
      case 'Blob':
        switch (typeof val) {
          case 'boolean':
          case 'number':
            throw typeError('only base-64-encoded strings are convertible to bytes')
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.nio.ByteBuffer')
            throw typeError('Start of structure or map found where not expected')
        }
        if (val.length % 4)
          throw typeError('Base64 encoded length is expected a multiple of 4 bytes but found: ' + val.length)
        // TODO: need a better check than this...
        if (Buffer.from(val, 'base64').toString('base64') != val)
          throw typeError('Invalid last non-pad Base64 character dectected')
        return val
      case 'List':
        switch (typeof val) {
          case 'boolean':
          case 'number':
          case 'string':
            throw typeError('Unexpected field type')
          case 'object':
            if (!Array.isArray(val)) throw typeError('Start of structure or map found where not expected')
        }
        return val.map(function(child) { return checkType(child, children) })
      case 'ParameterizedList':
        switch (typeof val) {
          case 'boolean':
          case 'number':
          case 'string':
            throw typeError("class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to class java.lang.Class (sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl and java.lang.Class are in module java.base of loader 'bootstrap')")
          case 'object':
            if (!Array.isArray(val)) throw typeError('Start of structure or map found where not expected')
        }
        return val.map(function(child) { return checkType(child, children) })
      case 'ParameterizedMap':
        switch (typeof val) {
          case 'boolean':
          case 'number':
          case 'string':
            throw typeError("class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to class java.lang.Class (sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl and java.lang.Class are in module java.base of loader 'bootstrap')")
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type java.util.Map<java.lang.String, com.amazonaws.dynamodb.v20120810.AttributeValue>')
        }
        return Object.keys(val).reduce(function(newVal, key) {
          newVal[key] = checkType(val[key], children)
          return newVal
        }, {})
      case 'Map':
        switch (typeof val) {
          case 'boolean':
          case 'number':
          case 'string':
            throw typeError('Unexpected field type')
          case 'object':
            if (Array.isArray(val)) {
              throw typeError('Unrecognized collection type java.util.Map<java.lang.String, ' +
                (~subtype.indexOf('.') ? subtype : 'com.amazonaws.dynamodb.v20120810.' + subtype) + '>')
            }
        }
        return Object.keys(val).reduce(function(newVal, key) {
          newVal[key] = checkType(val[key], children)
          return newVal
        }, {})
      case 'ValueStruct':
        switch (typeof val) {
          case 'boolean':
          case 'number':
          case 'string':
            throw typeError('Unexpected value type in payload')
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type class com.amazonaws.dynamodb.v20120810.' + subtype)
        }
        return checkTypes(val, children)
      case 'FieldStruct':
        switch (typeof val) {
          case 'boolean':
          case 'number':
          case 'string':
            throw typeError('Unexpected field type')
          case 'object':
            if (Array.isArray(val)) throw typeError('Unrecognized collection type class com.amazonaws.dynamodb.v20120810.' + subtype)
        }
        return checkTypes(val, children)
      default:
        throw new Error('Unknown type: ' + type)
    }
  }
}

var validateFns = {}

function checkValidations(data, validations, custom, store) {
  var attr, msg, errors = []

  for (attr in validations) {
    if (validations[attr].required && data[attr] == null) {
      throw db.validationError('The parameter \'' + attr + '\' is required but was not present in the request')
    }
    if (validations[attr].tableName) {
      msg = validateTableName(attr, data[attr])
      if (msg) throw db.validationError(msg)
    }
  }

  function checkNonRequireds(data, types, parent) {
    for (attr in types) {
      checkNonRequired(attr, data[attr], types[attr], parent)
    }
  }

  checkNonRequireds(data, validations)

  function checkNonRequired(attr, data, validations, parent) {
    if (validations == null || typeof validations != 'object') return
    for (var validation in validations) {
      if (errors.length >= 10) return
      if (~['type', 'required', 'tableName'].indexOf(validation)) continue
      if (validation != 'notNull' && data == null) continue
      if (validation == 'children') {
        if (/List$/.test(validations.type)) {
          for (var i = 0; i < data.length; i++) {
            checkNonRequired('member', data[i], validations.children,
              (parent ? parent + '.' : '') + toLowerFirst(attr) + '.' + (i + 1))
          }
          continue
        } else if (/Map/.test(validations.type)) {
          Object.keys(data).forEach(function(key) { // eslint-disable-line no-loop-func
            checkNonRequired('member', data[key], validations.children,
              (parent ? parent + '.' : '') + toLowerFirst(attr) + '.' + key)
          })
          continue
        }
        checkNonRequireds(data, validations.children, (parent ? parent + '.' : '') + toLowerFirst(attr))
        continue
      }
      validateFns[validation](parent, attr, validations[validation], data, errors)
    }
  }

  if (errors.length)
    throw db.validationError(errors.length + ' validation error' + (errors.length > 1 ? 's' : '') + ' detected: ' + errors.join('; '))

  if (custom) {
    msg = custom(data, store)
    if (msg) throw db.validationError(msg)
  }
}

validateFns.notNull = function(parent, key, val, data, errors) {
  validate(data != null, 'Member must not be null', data, parent, key, errors)
}
validateFns.greaterThanOrEqual = function(parent, key, val, data, errors) {
  validate(data >= val, 'Member must have value greater than or equal to ' + val, data, parent, key, errors)
}
validateFns.lessThanOrEqual = function(parent, key, val, data, errors) {
  validate(data <= val, 'Member must have value less than or equal to ' + val, data, parent, key, errors)
}
validateFns.regex = function(parent, key, pattern, data, errors) {
  validate(RegExp('^' + pattern + '$').test(data), 'Member must satisfy regular expression pattern: ' + pattern, data, parent, key, errors)
}
validateFns.lengthGreaterThanOrEqual = function(parent, key, val, data, errors) {
  var length = (typeof data == 'object' && !Array.isArray(data)) ? Object.keys(data).length : data.length
  validate(length >= val, 'Member must have length greater than or equal to ' + val, data, parent, key, errors)
}
validateFns.lengthLessThanOrEqual = function(parent, key, val, data, errors) {
  var length = (typeof data == 'object' && !Array.isArray(data)) ? Object.keys(data).length : data.length
  validate(length <= val, 'Member must have length less than or equal to ' + val, data, parent, key, errors)
}
validateFns.enum = function(parent, key, val, data, errors) {
  validate(~val.indexOf(data), 'Member must satisfy enum value set: [' + val.join(', ') + ']', data, parent, key, errors)
}
validateFns.keys = function(parent, key, val, data, errors) {
  Object.keys(data).forEach(function(mapKey) {
    try {
      Object.keys(val).forEach(function(validation) {
        validateFns[validation]('', '', val[validation], mapKey, [])
      })
    } catch (e) {
      var msgs = Object.keys(val).map(function(validation) {
        if (validation == 'lengthGreaterThanOrEqual')
          return 'Member must have length greater than or equal to ' + val[validation]
        if (validation == 'lengthLessThanOrEqual')
          return 'Member must have length less than or equal to ' + val[validation]
        if (validation == 'regex')
          return 'Member must satisfy regular expression pattern: ' + val[validation]
      })
      validate(false, 'Map keys must satisfy constraint: [' + msgs.join(', ') + ']', data, parent, key, errors)
    }
  })
}
validateFns.values = function(parent, key, val, data, errors) {
  Object.keys(data).forEach(function(mapKey) {
    try {
      Object.keys(val).forEach(function(validation) {
        validateFns[validation]('', '', val[validation], data[mapKey], [])
      })
    } catch (e) {
      var msgs = Object.keys(val).map(function(validation) {
        if (validation == 'lengthGreaterThanOrEqual')
          return 'Member must have length greater than or equal to ' + val[validation]
        if (validation == 'lengthLessThanOrEqual')
          return 'Member must have length less than or equal to ' + val[validation]
      })
      validate(false, 'Map value must satisfy constraint: [' + msgs.join(', ') + ']', data, parent, key, errors)
    }
  })
}

function validate(predicate, msg, data, parent, key, errors) {
  if (predicate) return
  var value = valueStr(data)
  if (value != 'null') value = '\'' + value + '\''
  parent = parent ? parent + '.' : ''
  errors.push('Value ' + value + ' at \'' + parent + toLowerFirst(key) + '\' failed to satisfy constraint: ' + msg)
}

function validateTableName(key, val) {
  if (val == null) return null
  if (val.length < 3 || val.length > 255)
    return key + ' must be at least 3 characters long and at most 255 characters long'
}

function toLowerFirst(str) {
  return str[0].toLowerCase() + str.slice(1)
}

function validateAttributeValue(value) {
  var types = Object.keys(value), msg, i, attr
  if (!types.length)
    return 'Supplied AttributeValue is empty, must contain exactly one of the supported datatypes'

  for (var type in value) {
    if (type == 'N') {
      msg = checkNum(type, value)
      if (msg) return msg
    }

    if (type == 'NULL' && !value[type])
      return 'One or more parameter values were invalid: Null attribute value types must have the value of true'

    if (type == 'SS' && !value[type].length)
      return 'One or more parameter values were invalid: An string set  may not be empty'

    if (type == 'NS' && !value[type].length)
      return 'One or more parameter values were invalid: An number set  may not be empty'

    if (type == 'BS' && !value[type].length)
      return 'One or more parameter values were invalid: Binary sets should not be empty'

    if (type == 'NS') {
      for (i = 0; i < value[type].length; i++) {
        msg = checkNum(i, value[type])
        if (msg) return msg
      }
    }

    if (type == 'SS' && findDuplicate(value[type]))
      return 'One or more parameter values were invalid: Input collection ' + valueStr(value[type]) + ' contains duplicates.'

    if (type == 'NS' && findDuplicate(value[type]))
      return 'Input collection contains duplicates'

    if (type == 'BS' && findDuplicate(value[type]))
      return 'One or more parameter values were invalid: Input collection ' + valueStr(value[type]) + 'of type BS contains duplicates.'

    if (type == 'M') {
      for (attr in value[type]) {
        msg = validateAttributeValue(value[type][attr])
        if (msg) return msg
      }
    }

    if (type == 'L') {
      for (i = 0; i < value[type].length; i++) {
        msg = validateAttributeValue(value[type][i])
        if (msg) return msg
      }
    }
  }

  if (types.length > 1)
    return 'Supplied AttributeValue has more than one datatypes set, must contain exactly one of the supported datatypes'
}

function checkNum(attr, obj) {
  if (!obj[attr])
    return 'The parameter cannot be converted to a numeric value'

  var bigNum
  try {
    bigNum = new Big(obj[attr])
  } catch (e) {
    return 'The parameter cannot be converted to a numeric value: ' + obj[attr]
  }
  if (bigNum.e > 125)
    return 'Number overflow. Attempting to store a number with magnitude larger than supported range'
  else if (bigNum.e < -130)
    return 'Number underflow. Attempting to store a number with magnitude smaller than supported range'
  else if (bigNum.c.length > 38)
    return 'Attempting to store more than 38 significant digits in a Number'

  obj[attr] = bigNum.toFixed()
}

function valueStr(data) {
  return data == null ? 'null' : Array.isArray(data) ? '[' + data.map(valueStr).join(', ') + ']' :
    typeof data == 'object' ? JSON.stringify(data) : data
}

function findDuplicate(arr) {
  if (!arr) return null
  var vals = Object.create(null)
  for (var i = 0; i < arr.length; i++) {
    if (vals[arr[i]]) return arr[i]
    vals[arr[i]] = true
  }
}

function validateAttributeConditions(data) {
  for (var key in data.Expected) {
    var condition = data.Expected[key]

    if ('AttributeValueList' in condition && 'Value' in condition)
      return 'One or more parameter values were invalid: ' +
        'Value and AttributeValueList cannot be used together for Attribute: ' + key

    if ('ComparisonOperator' in condition) {
      if ('Exists' in condition)
        return 'One or more parameter values were invalid: ' +
          'Exists and ComparisonOperator cannot be used together for Attribute: ' + key

      if (condition.ComparisonOperator != 'NULL' && condition.ComparisonOperator != 'NOT_NULL' &&
          !('AttributeValueList' in condition) && !('Value' in condition))
        return 'One or more parameter values were invalid: ' +
          'Value or AttributeValueList must be used with ComparisonOperator: ' + condition.ComparisonOperator +
          ' for Attribute: ' + key

      var values = condition.AttributeValueList ?
        condition.AttributeValueList.length : condition.Value ? 1 : 0
      var validAttrCount = false

      switch (condition.ComparisonOperator) {
        case 'EQ':
        case 'NE':
        case 'LE':
        case 'LT':
        case 'GE':
        case 'GT':
        case 'CONTAINS':
        case 'NOT_CONTAINS':
        case 'BEGINS_WITH':
          if (values === 1) validAttrCount = true
          break
        case 'NOT_NULL':
        case 'NULL':
          if (values === 0) validAttrCount = true
          break
        case 'IN':
          if (values > 0) validAttrCount = true
          break
        case 'BETWEEN':
          if (values === 2) validAttrCount = true
          break
      }
      if (!validAttrCount)
        return 'One or more parameter values were invalid: ' +
          'Invalid number of argument(s) for the ' + condition.ComparisonOperator + ' ComparisonOperator'

      if (condition.AttributeValueList && condition.AttributeValueList.length) {
        var type = Object.keys(condition.AttributeValueList[0])[0]
        if (condition.AttributeValueList.some(function(attr) { return Object.keys(attr)[0] != type })) {
          return 'One or more parameter values were invalid: AttributeValues inside AttributeValueList must be of same type'
        }
        if (condition.ComparisonOperator == 'BETWEEN' && db.compare('GT', condition.AttributeValueList[0], condition.AttributeValueList[1])) {
          return 'The BETWEEN condition was provided a range where the lower bound is greater than the upper bound'
        }
      }
    } else if ('AttributeValueList' in condition) {
      return 'One or more parameter values were invalid: ' +
        'AttributeValueList can only be used with a ComparisonOperator for Attribute: ' + key
    } else {
      var exists = condition.Exists == null || condition.Exists
      if (exists && condition.Value == null)
        return 'One or more parameter values were invalid: ' +
          'Value must be provided when Exists is ' +
          (condition.Exists == null ? 'null' : condition.Exists) +
          ' for Attribute: ' + key
      else if (!exists && condition.Value != null)
        return 'One or more parameter values were invalid: ' +
          'Value cannot be used when Exists is false for Attribute: ' + key
      if (condition.Value != null) {
        var msg = validateAttributeValue(condition.Value)
        if (msg) return msg
      }
    }
  }
}

function validateConditions(conditions) {
  var lengths = {
    NULL: 0,
    NOT_NULL: 0,
    EQ: 1,
    NE: 1,
    LE: 1,
    LT: 1,
    GE: 1,
    GT: 1,
    CONTAINS: 1,
    NOT_CONTAINS: 1,
    BEGINS_WITH: 1,
    IN: [1],
    BETWEEN: 2,
  }
  var types = {
    EQ: ['S', 'N', 'B', 'SS', 'NS', 'BS'],
    NE: ['S', 'N', 'B', 'SS', 'NS', 'BS'],
    LE: ['S', 'N', 'B'],
    LT: ['S', 'N', 'B'],
    GE: ['S', 'N', 'B'],
    GT: ['S', 'N', 'B'],
    CONTAINS: ['S', 'N', 'B'],
    NOT_CONTAINS: ['S', 'N', 'B'],
    BEGINS_WITH: ['S', 'B'],
    IN: ['S', 'N', 'B'],
    BETWEEN: ['S', 'N', 'B'],
  }
  for (var key in conditions) {
    var comparisonOperator = conditions[key].ComparisonOperator
    var attrValList = conditions[key].AttributeValueList || []
    for (var i = 0; i < attrValList.length; i++) {
      var msg = validateAttributeValue(attrValList[i])
      if (msg) return msg
    }

    if ((typeof lengths[comparisonOperator] == 'number' && attrValList.length != lengths[comparisonOperator]) ||
        (attrValList.length < lengths[comparisonOperator][0] || attrValList.length > lengths[comparisonOperator][1]))
      return 'One or more parameter values were invalid: Invalid number of argument(s) for the ' +
        comparisonOperator + ' ComparisonOperator'

    if (attrValList.length) {
      var type = Object.keys(attrValList[0])[0]
      if (attrValList.some(function(attr) { return Object.keys(attr)[0] != type })) {
        return 'One or more parameter values were invalid: AttributeValues inside AttributeValueList must be of same type'
      }
    }

    if (types[comparisonOperator]) {
      for (i = 0; i < attrValList.length; i++) {
        if (!~types[comparisonOperator].indexOf(Object.keys(attrValList[i])[0]))
          return 'One or more parameter values were invalid: ComparisonOperator ' + comparisonOperator +
            ' is not valid for ' + Object.keys(attrValList[i])[0] + ' AttributeValue type'
      }
    }

    if (comparisonOperator == 'BETWEEN' && db.compare('GT', attrValList[0], attrValList[1])) {
      return 'The BETWEEN condition was provided a range where the lower bound is greater than the upper bound'
    }
  }
}

function validateExpressionParams(data, expressions, nonExpressions) {
  var exprParams = expressions.filter(function(expr) { return data[expr] != null })

  if (exprParams.length) {
    // Special case for KeyConditions and KeyConditionExpression
    if (data.KeyConditions != null && data.KeyConditionExpression == null) {
      nonExpressions.splice(nonExpressions.indexOf('KeyConditions'), 1)
    }
    var nonExprParams = nonExpressions.filter(function(expr) { return data[expr] != null })
    if (nonExprParams.length) {
      return 'Can not use both expression and non-expression parameters in the same request: ' +
        'Non-expression parameters: {' + nonExprParams.join(', ') + '} ' +
        'Expression parameters: {' + exprParams.join(', ') + '}'
    }
  }

  if (data.ExpressionAttributeNames != null && !exprParams.length) {
    return 'ExpressionAttributeNames can only be specified when using expressions'
  }

  var valExprs = expressions.filter(function(expr) { return expr != 'ProjectionExpression' })
  if (valExprs.length && data.ExpressionAttributeValues != null &&
      valExprs.every(function(expr) { return data[expr] == null })) {
    return 'ExpressionAttributeValues can only be specified when using expressions: ' +
      valExprs.join(' and ') + ' ' + (valExprs.length > 1 ? 'are' : 'is') + ' null'
  }
}

function validateExpressions(data) {
  var key, msg, result, context = {
    attrNames: data.ExpressionAttributeNames,
    attrVals: data.ExpressionAttributeValues,
    unusedAttrNames: {},
    unusedAttrVals: {},
  }

  if (data.ExpressionAttributeNames != null) {
    if (!Object.keys(data.ExpressionAttributeNames).length)
      return 'ExpressionAttributeNames must not be empty'
    for (key in data.ExpressionAttributeNames) {
      if (!/^#[0-9a-zA-Z_]+$/.test(key)) {
        return 'ExpressionAttributeNames contains invalid key: Syntax error; key: "' + key + '"'
      }
      context.unusedAttrNames[key] = true
    }
  }

  if (data.ExpressionAttributeValues != null) {
    if (!Object.keys(data.ExpressionAttributeValues).length)
      return 'ExpressionAttributeValues must not be empty'
    for (key in data.ExpressionAttributeValues) {
      if (!/^:[0-9a-zA-Z_]+$/.test(key)) {
        return 'ExpressionAttributeValues contains invalid key: Syntax error; key: "' + key + '"'
      }
      context.unusedAttrVals[key] = true
    }
    for (key in data.ExpressionAttributeValues) {
      msg = validateAttributeValue(data.ExpressionAttributeValues[key])
      if (msg) {
        msg = 'ExpressionAttributeValues contains invalid value: ' + msg + ' for key ' + key
        return msg
      }
    }
  }

  if (data.UpdateExpression != null) {
    result = parse(data.UpdateExpression, updateParser, context)
    if (typeof result == 'string') {
      return 'Invalid UpdateExpression: ' + result
    }
    data._updates = result
  }

  if (data.ConditionExpression != null) {
    result = parse(data.ConditionExpression, conditionParser, context)
    if (typeof result == 'string') {
      return 'Invalid ConditionExpression: ' + result
    }
    data._condition = result
  }

  if (data.KeyConditionExpression != null) {
    context.isKeyCondition = true
    result = parse(data.KeyConditionExpression, conditionParser, context)
    if (typeof result == 'string') {
      return 'Invalid KeyConditionExpression: ' + result
    }
    data._keyCondition = result
  }

  if (data.FilterExpression != null) {
    result = parse(data.FilterExpression, conditionParser, context)
    if (typeof result == 'string') {
      return 'Invalid FilterExpression: ' + result
    }
    data._filter = result
  }

  if (data.ProjectionExpression != null) {
    result = parse(data.ProjectionExpression, projectionParser, context)
    if (typeof result == 'string') {
      return 'Invalid ProjectionExpression: ' + result
    }
    data._projection = result
  }

  if (Object.keys(context.unusedAttrNames).length) {
    return 'Value provided in ExpressionAttributeNames unused in expressions: ' +
      'keys: {' + Object.keys(context.unusedAttrNames).join(', ') + '}'
  }

  if (Object.keys(context.unusedAttrVals).length) {
    return 'Value provided in ExpressionAttributeValues unused in expressions: ' +
      'keys: {' + Object.keys(context.unusedAttrVals).join(', ') + '}'
  }
}

function parse(str, parser, context) {
  if (str == '') return 'The expression can not be empty;'
  context.isReserved = isReserved
  context.compare = db.compare
  try {
    return parser.parse(str, {context: context})
  } catch (e) {
    return e.name == 'SyntaxError' ? 'Syntax error; ' + e.message : e.message
  }
}

function convertKeyCondition(expression) {
  var keyConds = Object.create(null)
  return checkExpr(expression, keyConds) || keyConds
}

function checkExpr(expr, keyConds) {
  if (!expr || !expr.type) return
  if (~['or', 'not', 'in', '<>'].indexOf(expr.type)) {
    return 'Invalid operator used in KeyConditionExpression: ' + expr.type.toUpperCase()
  }
  if (expr.type == 'function' && ~['attribute_exists', 'attribute_not_exists', 'attribute_type', 'contains'].indexOf(expr.name)) {
    return 'Invalid operator used in KeyConditionExpression: ' + expr.name
  }
  if (expr.type == 'function' && expr.name == 'size') {
    return 'KeyConditionExpressions cannot contain nested operations'
  }
  if (expr.type == 'between' && !Array.isArray(expr.args[0])) {
    return 'Invalid condition in KeyConditionExpression: ' + expr.type.toUpperCase() + ' operator must have the key attribute as its first operand'
  }
  if (expr.type == 'function' && expr.name == 'begins_with' && !Array.isArray(expr.args[0])) {
    return 'Invalid condition in KeyConditionExpression: ' + expr.name + ' operator must have the key attribute as its first operand'
  }
  if (expr.args) {
    var attrName = '', attrIx = 0
    for (var i = 0; i < expr.args.length; i++) {
      if (Array.isArray(expr.args[i])) {
        if (attrName) {
          return 'Invalid condition in KeyConditionExpression: Multiple attribute names used in one condition'
        }
        if (expr.args[i].length > 1) {
          return 'KeyConditionExpressions cannot have conditions on nested attributes'
        }
        attrName = expr.args[i][0]
        attrIx = i
      } else if (expr.args[i].type) {
        var result = checkExpr(expr.args[i], keyConds)
        if (result) return result
      }
    }
    if (expr.type != 'and') {
      if (!attrName) {
        return 'Invalid condition in KeyConditionExpression: No key attribute specified'
      }
      if (keyConds[attrName]) {
        return 'KeyConditionExpressions must only contain one condition per key'
      }
      if (attrIx != 0) {
        expr.type = {
          '=': '=',
          '<': '>',
          '<=': '>=',
          '>': '<',
          '>=': '<=',
        }[expr.type]
        expr.args[1] = expr.args[0]
      }
      keyConds[attrName] = {
        ComparisonOperator: {
          '=': 'EQ',
          '<': 'LT',
          '<=': 'LE',
          '>': 'GT',
          '>=': 'GE',
          'between': 'BETWEEN',
          'function': 'BEGINS_WITH',
        }[expr.type],
        AttributeValueList: expr.args.slice(1),
      }
    }
  }
}

// http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
var RESERVED_WORDS = {
  ABORT: true,
  ABSOLUTE: true,
  ACTION: true,
  ADD: true,
  AFTER: true,
  AGENT: true,
  AGGREGATE: true,
  ALL: true,
  ALLOCATE: true,
  ALTER: true,
  ANALYZE: true,
  AND: true,
  ANY: true,
  ARCHIVE: true,
  ARE: true,
  ARRAY: true,
  AS: true,
  ASC: true,
  ASCII: true,
  ASENSITIVE: true,
  ASSERTION: true,
  ASYMMETRIC: true,
  AT: true,
  ATOMIC: true,
  ATTACH: true,
  ATTRIBUTE: true,
  AUTH: true,
  AUTHORIZATION: true,
  AUTHORIZE: true,
  AUTO: true,
  AVG: true,
  BACK: true,
  BACKUP: true,
  BASE: true,
  BATCH: true,
  BEFORE: true,
  BEGIN: true,
  BETWEEN: true,
  BIGINT: true,
  BINARY: true,
  BIT: true,
  BLOB: true,
  BLOCK: true,
  BOOLEAN: true,
  BOTH: true,
  BREADTH: true,
  BUCKET: true,
  BULK: true,
  BY: true,
  BYTE: true,
  CALL: true,
  CALLED: true,
  CALLING: true,
  CAPACITY: true,
  CASCADE: true,
  CASCADED: true,
  CASE: true,
  CAST: true,
  CATALOG: true,
  CHAR: true,
  CHARACTER: true,
  CHECK: true,
  CLASS: true,
  CLOB: true,
  CLOSE: true,
  CLUSTER: true,
  CLUSTERED: true,
  CLUSTERING: true,
  CLUSTERS: true,
  COALESCE: true,
  COLLATE: true,
  COLLATION: true,
  COLLECTION: true,
  COLUMN: true,
  COLUMNS: true,
  COMBINE: true,
  COMMENT: true,
  COMMIT: true,
  COMPACT: true,
  COMPILE: true,
  COMPRESS: true,
  CONDITION: true,
  CONFLICT: true,
  CONNECT: true,
  CONNECTION: true,
  CONSISTENCY: true,
  CONSISTENT: true,
  CONSTRAINT: true,
  CONSTRAINTS: true,
  CONSTRUCTOR: true,
  CONSUMED: true,
  CONTINUE: true,
  CONVERT: true,
  COPY: true,
  CORRESPONDING: true,
  COUNT: true,
  COUNTER: true,
  CREATE: true,
  CROSS: true,
  CUBE: true,
  CURRENT: true,
  CURSOR: true,
  CYCLE: true,
  DATA: true,
  DATABASE: true,
  DATE: true,
  DATETIME: true,
  DAY: true,
  DEALLOCATE: true,
  DEC: true,
  DECIMAL: true,
  DECLARE: true,
  DEFAULT: true,
  DEFERRABLE: true,
  DEFERRED: true,
  DEFINE: true,
  DEFINED: true,
  DEFINITION: true,
  DELETE: true,
  DELIMITED: true,
  DEPTH: true,
  DEREF: true,
  DESC: true,
  DESCRIBE: true,
  DESCRIPTOR: true,
  DETACH: true,
  DETERMINISTIC: true,
  DIAGNOSTICS: true,
  DIRECTORIES: true,
  DISABLE: true,
  DISCONNECT: true,
  DISTINCT: true,
  DISTRIBUTE: true,
  DO: true,
  DOMAIN: true,
  DOUBLE: true,
  DROP: true,
  DUMP: true,
  DURATION: true,
  DYNAMIC: true,
  EACH: true,
  ELEMENT: true,
  ELSE: true,
  ELSEIF: true,
  EMPTY: true,
  ENABLE: true,
  END: true,
  EQUAL: true,
  EQUALS: true,
  ERROR: true,
  ESCAPE: true,
  ESCAPED: true,
  EVAL: true,
  EVALUATE: true,
  EXCEEDED: true,
  EXCEPT: true,
  EXCEPTION: true,
  EXCEPTIONS: true,
  EXCLUSIVE: true,
  EXEC: true,
  EXECUTE: true,
  EXISTS: true,
  EXIT: true,
  EXPLAIN: true,
  EXPLODE: true,
  EXPORT: true,
  EXPRESSION: true,
  EXTENDED: true,
  EXTERNAL: true,
  EXTRACT: true,
  FAIL: true,
  FALSE: true,
  FAMILY: true,
  FETCH: true,
  FIELDS: true,
  FILE: true,
  FILTER: true,
  FILTERING: true,
  FINAL: true,
  FINISH: true,
  FIRST: true,
  FIXED: true,
  FLATTERN: true,
  FLOAT: true,
  FOR: true,
  FORCE: true,
  FOREIGN: true,
  FORMAT: true,
  FORWARD: true,
  FOUND: true,
  FREE: true,
  FROM: true,
  FULL: true,
  FUNCTION: true,
  FUNCTIONS: true,
  GENERAL: true,
  GENERATE: true,
  GET: true,
  GLOB: true,
  GLOBAL: true,
  GO: true,
  GOTO: true,
  GRANT: true,
  GREATER: true,
  GROUP: true,
  GROUPING: true,
  HANDLER: true,
  HASH: true,
  HAVE: true,
  HAVING: true,
  HEAP: true,
  HIDDEN: true,
  HOLD: true,
  HOUR: true,
  IDENTIFIED: true,
  IDENTITY: true,
  IF: true,
  IGNORE: true,
  IMMEDIATE: true,
  IMPORT: true,
  IN: true,
  INCLUDING: true,
  INCLUSIVE: true,
  INCREMENT: true,
  INCREMENTAL: true,
  INDEX: true,
  INDEXED: true,
  INDEXES: true,
  INDICATOR: true,
  INFINITE: true,
  INITIALLY: true,
  INLINE: true,
  INNER: true,
  INNTER: true,
  INOUT: true,
  INPUT: true,
  INSENSITIVE: true,
  INSERT: true,
  INSTEAD: true,
  INT: true,
  INTEGER: true,
  INTERSECT: true,
  INTERVAL: true,
  INTO: true,
  INVALIDATE: true,
  IS: true,
  ISOLATION: true,
  ITEM: true,
  ITEMS: true,
  ITERATE: true,
  JOIN: true,
  KEY: true,
  KEYS: true,
  LAG: true,
  LANGUAGE: true,
  LARGE: true,
  LAST: true,
  LATERAL: true,
  LEAD: true,
  LEADING: true,
  LEAVE: true,
  LEFT: true,
  LENGTH: true,
  LESS: true,
  LEVEL: true,
  LIKE: true,
  LIMIT: true,
  LIMITED: true,
  LINES: true,
  LIST: true,
  LOAD: true,
  LOCAL: true,
  LOCALTIME: true,
  LOCALTIMESTAMP: true,
  LOCATION: true,
  LOCATOR: true,
  LOCK: true,
  LOCKS: true,
  LOG: true,
  LOGED: true,
  LONG: true,
  LOOP: true,
  LOWER: true,
  MAP: true,
  MATCH: true,
  MATERIALIZED: true,
  MAX: true,
  MAXLEN: true,
  MEMBER: true,
  MERGE: true,
  METHOD: true,
  METRICS: true,
  MIN: true,
  MINUS: true,
  MINUTE: true,
  MISSING: true,
  MOD: true,
  MODE: true,
  MODIFIES: true,
  MODIFY: true,
  MODULE: true,
  MONTH: true,
  MULTI: true,
  MULTISET: true,
  NAME: true,
  NAMES: true,
  NATIONAL: true,
  NATURAL: true,
  NCHAR: true,
  NCLOB: true,
  NEW: true,
  NEXT: true,
  NO: true,
  NONE: true,
  NOT: true,
  NULL: true,
  NULLIF: true,
  NUMBER: true,
  NUMERIC: true,
  OBJECT: true,
  OF: true,
  OFFLINE: true,
  OFFSET: true,
  OLD: true,
  ON: true,
  ONLINE: true,
  ONLY: true,
  OPAQUE: true,
  OPEN: true,
  OPERATOR: true,
  OPTION: true,
  OR: true,
  ORDER: true,
  ORDINALITY: true,
  OTHER: true,
  OTHERS: true,
  OUT: true,
  OUTER: true,
  OUTPUT: true,
  OVER: true,
  OVERLAPS: true,
  OVERRIDE: true,
  OWNER: true,
  PAD: true,
  PARALLEL: true,
  PARAMETER: true,
  PARAMETERS: true,
  PARTIAL: true,
  PARTITION: true,
  PARTITIONED: true,
  PARTITIONS: true,
  PATH: true,
  PERCENT: true,
  PERCENTILE: true,
  PERMISSION: true,
  PERMISSIONS: true,
  PIPE: true,
  PIPELINED: true,
  PLAN: true,
  POOL: true,
  POSITION: true,
  PRECISION: true,
  PREPARE: true,
  PRESERVE: true,
  PRIMARY: true,
  PRIOR: true,
  PRIVATE: true,
  PRIVILEGES: true,
  PROCEDURE: true,
  PROCESSED: true,
  PROJECT: true,
  PROJECTION: true,
  PROPERTY: true,
  PROVISIONING: true,
  PUBLIC: true,
  PUT: true,
  QUERY: true,
  QUIT: true,
  QUORUM: true,
  RAISE: true,
  RANDOM: true,
  RANGE: true,
  RANK: true,
  RAW: true,
  READ: true,
  READS: true,
  REAL: true,
  REBUILD: true,
  RECORD: true,
  RECURSIVE: true,
  REDUCE: true,
  REF: true,
  REFERENCE: true,
  REFERENCES: true,
  REFERENCING: true,
  REGEXP: true,
  REGION: true,
  REINDEX: true,
  RELATIVE: true,
  RELEASE: true,
  REMAINDER: true,
  RENAME: true,
  REPEAT: true,
  REPLACE: true,
  REQUEST: true,
  RESET: true,
  RESIGNAL: true,
  RESOURCE: true,
  RESPONSE: true,
  RESTORE: true,
  RESTRICT: true,
  RESULT: true,
  RETURN: true,
  RETURNING: true,
  RETURNS: true,
  REVERSE: true,
  REVOKE: true,
  RIGHT: true,
  ROLE: true,
  ROLES: true,
  ROLLBACK: true,
  ROLLUP: true,
  ROUTINE: true,
  ROW: true,
  ROWS: true,
  RULE: true,
  RULES: true,
  SAMPLE: true,
  SATISFIES: true,
  SAVE: true,
  SAVEPOINT: true,
  SCAN: true,
  SCHEMA: true,
  SCOPE: true,
  SCROLL: true,
  SEARCH: true,
  SECOND: true,
  SECTION: true,
  SEGMENT: true,
  SEGMENTS: true,
  SELECT: true,
  SELF: true,
  SEMI: true,
  SENSITIVE: true,
  SEPARATE: true,
  SEQUENCE: true,
  SERIALIZABLE: true,
  SESSION: true,
  SET: true,
  SETS: true,
  SHARD: true,
  SHARE: true,
  SHARED: true,
  SHORT: true,
  SHOW: true,
  SIGNAL: true,
  SIMILAR: true,
  SIZE: true,
  SKEWED: true,
  SMALLINT: true,
  SNAPSHOT: true,
  SOME: true,
  SOURCE: true,
  SPACE: true,
  SPACES: true,
  SPARSE: true,
  SPECIFIC: true,
  SPECIFICTYPE: true,
  SPLIT: true,
  SQL: true,
  SQLCODE: true,
  SQLERROR: true,
  SQLEXCEPTION: true,
  SQLSTATE: true,
  SQLWARNING: true,
  START: true,
  STATE: true,
  STATIC: true,
  STATUS: true,
  STORAGE: true,
  STORE: true,
  STORED: true,
  STREAM: true,
  STRING: true,
  STRUCT: true,
  STYLE: true,
  SUB: true,
  SUBMULTISET: true,
  SUBPARTITION: true,
  SUBSTRING: true,
  SUBTYPE: true,
  SUM: true,
  SUPER: true,
  SYMMETRIC: true,
  SYNONYM: true,
  SYSTEM: true,
  TABLE: true,
  TABLESAMPLE: true,
  TEMP: true,
  TEMPORARY: true,
  TERMINATED: true,
  TEXT: true,
  THAN: true,
  THEN: true,
  THROUGHPUT: true,
  TIME: true,
  TIMESTAMP: true,
  TIMEZONE: true,
  TINYINT: true,
  TO: true,
  TOKEN: true,
  TOTAL: true,
  TOUCH: true,
  TRAILING: true,
  TRANSACTION: true,
  TRANSFORM: true,
  TRANSLATE: true,
  TRANSLATION: true,
  TREAT: true,
  TRIGGER: true,
  TRIM: true,
  TRUE: true,
  TRUNCATE: true,
  TTL: true,
  TUPLE: true,
  TYPE: true,
  UNDER: true,
  UNDO: true,
  UNION: true,
  UNIQUE: true,
  UNIT: true,
  UNKNOWN: true,
  UNLOGGED: true,
  UNNEST: true,
  UNPROCESSED: true,
  UNSIGNED: true,
  UNTIL: true,
  UPDATE: true,
  UPPER: true,
  URL: true,
  USAGE: true,
  USE: true,
  USER: true,
  USERS: true,
  USING: true,
  UUID: true,
  VACUUM: true,
  VALUE: true,
  VALUED: true,
  VALUES: true,
  VARCHAR: true,
  VARIABLE: true,
  VARIANCE: true,
  VARINT: true,
  VARYING: true,
  VIEW: true,
  VIEWS: true,
  VIRTUAL: true,
  VOID: true,
  WAIT: true,
  WHEN: true,
  WHENEVER: true,
  WHERE: true,
  WHILE: true,
  WINDOW: true,
  WITH: true,
  WITHIN: true,
  WITHOUT: true,
  WORK: true,
  WRAPPED: true,
  WRITE: true,
  YEAR: true,
  ZONE: true,
}

function isReserved(name) {
  return RESERVED_WORDS[name.toUpperCase()] != null
}
