var util = require('util')

var adb = require('adbkit')
var Promise = require('bluebird')

var keyutil = module.exports = Object.create(null)

keyutil.parseKeyCharacterMap = function(stream) {
  var resolver = Promise.defer()
    , state = 'type_t'
    , keymap = {
        type: null
      , keys: []
      }
    , lastKey
    , lastRule
    , lastModifier
    , lastBehavior

  function fail(char, state) {
    throw new Error(util.format(
      'Unexpected character "%s" in state "%s"'
    , char
    , state
    ))
  }

  function parse(char) {
    switch (state) {
      case 'comment_before_type_t':
        if (char === '\n') {
          state = 'type_t'
          break
        }
        return true
      case 'type_t':
        if (char === '\n') {
          return true
        }
        if (char === '#') {
          state = 'comment_before_type_t'
          return true
        }
        if (char === 'k') {
          state = 'key_k'
          return parse(char)
        }
        if (char === 't') {
          state = 'type_y'
          return true
        }
        return fail(char, state)
      case 'type_y':
        if (char === 'y') {
          state = 'type_p'
          return true
        }
        return fail(char, state)
      case 'type_p':
        if (char === 'p') {
          state = 'type_e'
          return true
        }
        return fail(char, state)
      case 'type_e':
        if (char === 'e') {
          state = 'type_name_start'
          keymap.type = ''
          return true
        }
        return fail(char, state)
      case 'type_name_start':
        if (char === ' ') {
          return true
        }
        if (char >= 'A' && char <= 'Z') {
          keymap.type += char
          state = 'type_name_continued'
          return true
        }
        return fail(char, state)
      case 'type_name_continued':
        if (char === '\n') {
          // Could have more of these, although it doesn't make much sense
          state = 'type_t'
          return true
        }
        if (char >= 'A' && char <= 'Z') {
          keymap.type += char
          return true
        }
        return fail(char, state)
      case 'comment_before_key_k':
        if (char === '\n') {
          state = 'key_k'
          break
        }
        return true
      case 'key_k':
        if (char === '\n') {
          return true
        }
        if (char === '#') {
          state = 'comment_before_key_k'
          return true
        }
        if (char === 'k') {
          state = 'key_e'
          return true
        }
        return fail(char, state)
      case 'key_e':
        if (char === 'e') {
          state = 'key_y'
          return true
        }
        return fail(char, state)
      case 'key_y':
        if (char === 'y') {
          state = 'key_name_start'
          return true
        }
        return fail(char, state)
      case 'key_name_start':
        if (char === ' ') {
          return true
        }
        if ((char >= '0' && char <= '9') ||
            (char >= 'A' && char <= 'Z')) {
          keymap.keys.push(lastKey = {
            key: char
          , rules: []
          })
          state = 'key_name_continued'
          return true
        }
        return fail(char, state)
      case 'key_name_continued':
        if (char === ' ') {
          state = 'key_start_block'
          return true
        }
        if ((char >= '0' && char <= '9') ||
            (char >= 'A' && char <= 'Z') ||
            (char === '_')) {
          lastKey.key += char
          return true
        }
        return fail(char, state)
      case 'key_start_block':
        if (char === ' ') {
          return true
        }
        if (char === '{') {
          state = 'filter_name_start'
          return true
        }
        return fail(char, state)
      case 'filter_name_start':
        if (char === '\n' || char === '\t' || char === ' ') {
          return true
        }
        if (char === '}') {
          state = 'key_k'
          return true
        }
        if (char >= 'a' && char <= 'z') {
          lastKey.rules.push(lastRule = {
            modifiers: [lastModifier = {
              type: char
            }]
          , behaviors: []
          })
          state = 'filter_name_continued'
          return true
        }
        return fail(char, state)
      case 'filter_name_continued':
        if (char === ':') {
          state = 'filter_behavior_start'
          return true
        }
        if (char === ',') {
          state = 'filter_name_or_start'
          return true
        }
        if (char === '+') {
          state = 'filter_name_and_start'
          return true
        }
        if (char >= 'a' && char <= 'z') {
          lastModifier.type += char
          return true
        }
        return fail(char, state)
      case 'filter_name_or_start':
        if (char === ' ') {
          return true
        }
        if (char >= 'a' && char <= 'z') {
          lastKey.rules.push(lastRule = {
            modifiers: [lastModifier = {
              type: char
            }]
          , behaviors: lastRule.behaviors
          })
          state = 'filter_name_continued'
          return true
        }
        return fail(char, state)
      case 'filter_name_and_start':
        if (char === ' ') {
          return true
        }
        if (char >= 'a' && char <= 'z') {
          lastRule.modifiers.push(lastModifier = {
            type: char
          })
          state = 'filter_name_continued'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal':
        if (char === '\\') {
          state = 'filter_behavior_literal_escape'
          return true
        }
        if (char !== "'") {
          lastRule.behaviors.push({
            type: 'literal'
          , value: char
          })
          state = 'filter_behavior_literal_end'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal_escape':
        if (char === '\\' || char === '\'' || char === '"') {
          lastRule.behaviors.push({
            type: 'literal'
          , value: char
          })
          state = 'filter_behavior_literal_end'
          return true
        }
        if (char === 'n') {
          lastRule.behaviors.push({
            type: 'literal'
          , value: '\n'
          })
          state = 'filter_behavior_literal_end'
          return true
        }
        if (char === 't') {
          lastRule.behaviors.push({
            type: 'literal'
          , value: '\t'
          })
          state = 'filter_behavior_literal_end'
          return true
        }
        if (char === 'u') {
          state = 'filter_behavior_literal_unicode_1'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal_end':
        if (char === '\'') {
          state = 'filter_behavior_start'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_start':
        if (char === '\n') {
          state = 'filter_name_start'
          return true
        }
        if (char === ' ') {
          return true
        }
        if (char === "'") {
          state = 'filter_behavior_literal'
          return true
        }
        if (char === 'n') {
          state = 'filter_behavior_none_2'
          return true
        }
        if (char === 'f') {
          state = 'filter_behavior_fallback_2'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_2':
        if (char === 'a') {
          state = 'filter_behavior_fallback_3'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_3':
        if (char === 'l') {
          state = 'filter_behavior_fallback_4'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_4':
        if (char === 'l') {
          state = 'filter_behavior_fallback_5'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_5':
        if (char === 'b') {
          state = 'filter_behavior_fallback_6'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_6':
        if (char === 'a') {
          state = 'filter_behavior_fallback_7'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_7':
        if (char === 'c') {
          state = 'filter_behavior_fallback_8'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_8':
        if (char === 'k') {
          state = 'filter_behavior_fallback_key_start'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_key_start':
        if (char === ' ') {
          return true
        }
        if ((char >= '0' && char <= '9') ||
            (char >= 'A' && char <= 'Z')) {
          lastRule.behaviors.push(lastBehavior = {
            type: 'fallback'
          , key: char
          })
          state = 'filter_behavior_fallback_key_continued'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_fallback_key_continued':
        if (char === ' ') {
          state = 'filter_behavior_start'
          return true
        }
        if (char === '\n') {
          state = 'filter_name_start'
          return true
        }
        if ((char >= '0' && char <= '9') ||
            (char >= 'A' && char <= 'Z') ||
            (char === '_')) {
          lastBehavior.key += char
          return true
        }
        return fail(char, state)
      case 'filter_behavior_none_2':
        if (char === 'o') {
          state = 'filter_behavior_none_3'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_none_3':
        if (char === 'n') {
          state = 'filter_behavior_none_4'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_none_4':
        if (char === 'e') {
          lastRule.behaviors.push({
            type: 'none'
          })
          state = 'filter_behavior_start'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal_unicode_1':
        if ((char >= '0' && char <= '9') ||
            (char >= 'a' && char <= 'f')) {
          lastRule.behaviors.push(lastBehavior = {
            type: 'literal'
          , value: parseInt(char, 16) << 12
          })
          state = 'filter_behavior_literal_unicode_2'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal_unicode_2':
        if ((char >= '0' && char <= '9') ||
            (char >= 'a' && char <= 'f')) {
          lastBehavior.value += parseInt(char, 16) << 8
          state = 'filter_behavior_literal_unicode_3'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal_unicode_3':
        if ((char >= '0' && char <= '9') ||
            (char >= 'a' && char <= 'f')) {
          lastBehavior.value += parseInt(char, 16) << 4
          state = 'filter_behavior_literal_unicode_4'
          return true
        }
        return fail(char, state)
      case 'filter_behavior_literal_unicode_4':
        if ((char >= '0' && char <= '9') ||
            (char >= 'a' && char <= 'f')) {
          lastBehavior.value += parseInt(char, 16)
          lastBehavior.value = String.fromCharCode(lastBehavior.value)
          state = 'filter_behavior_literal_end'
          return true
        }
        return fail(char, state)
      default:
        throw new Error(util.format('Unexpected state "%s"', state))
    }
  }

  function errorListener(err) {
    resolver.reject(err)
  }

  function readableListener() {
    var chunk = stream.read()
      , i = 0
      , l = chunk.length

    try {
      while (i < l) {
        parse(String.fromCharCode(chunk[i++]))
      }
    }
    catch (err) {
      resolver.reject(err)
    }
  }

  function endListener() {
    resolver.resolve(keymap)
  }

  stream.on('error', errorListener)
  stream.on('readable', readableListener)
  stream.on('end', endListener)

  return resolver.promise.finally(function() {
    stream.removeListener('error', errorListener)
    stream.removeListener('readable', readableListener)
    stream.removeListener('end', endListener)
  })
}

keyutil.namedKey = function(name) {
  var key = adb.Keycode['KEYCODE_' + name.toUpperCase()]
  if (key === void 0) {
    throw new Error(util.format('Unknown key "%s"', name))
  }
  return key
}

keyutil.buildCharMap = function(keymap) {
  var charmap = Object.create(null)

  keymap.keys.forEach(function(key) {
    key.rules.forEach(function(rule) {
      var combination = {
        key: keyutil.namedKey(key.key)
      , modifiers: []
      , complexity: 0
      }

      var shouldHandle = rule.modifiers.every(function(modifier) {
        switch (modifier.type) {
          case 'label':
            return false // ignore
          case 'base':
            return true
          case 'shift':
          case 'lshift':
            combination.modifiers.push(adb.Keycode.KEYCODE_SHIFT_LEFT)
            combination.complexity += 10
            return true
          case 'rshift':
            combination.modifiers.push(adb.Keycode.KEYCODE_SHIFT_RIGHT)
            combination.complexity += 10
            return true
          case 'alt':
          case 'lalt':
            combination.modifiers.push(adb.Keycode.KEYCODE_ALT_LEFT)
            combination.complexity += 20
            return true
          case 'ralt':
            combination.modifiers.push(adb.Keycode.KEYCODE_ALT_RIGHT)
            combination.complexity += 20
            return true
          case 'ctrl':
          case 'lctrl':
            combination.modifiers.push(adb.Keycode.KEYCODE_CTRL_LEFT)
            combination.complexity += 20
            return true
          case 'rctrl':
            combination.modifiers.push(adb.Keycode.KEYCODE_CTRL_RIGHT)
            combination.complexity += 20
            return true
          case 'meta':
          case 'lmeta':
            combination.modifiers.push(adb.Keycode.KEYCODE_META_LEFT)
            combination.complexity += 20
            return true
          case 'rmeta':
            combination.modifiers.push(adb.Keycode.KEYCODE_META_RIGHT)
            combination.complexity += 20
            return true
          case 'sym':
            combination.modifiers.push(adb.Keycode.KEYCODE_SYM)
            combination.complexity += 10
            return true
          case 'fn':
            combination.modifiers.push(adb.Keycode.KEYCODE_FUNCTION)
            combination.complexity += 30
            return true
          case 'capslock':
            combination.modifiers.push(adb.Keycode.KEYCODE_CAPS_LOCK)
            combination.complexity += 30
            return true
          case 'numlock':
            combination.modifiers.push(adb.Keycode.KEYCODE_NUM_LOCK)
            combination.complexity += 30
            return true
          case 'scrolllock':
            combination.modifiers.push(adb.Keycode.KEYCODE_SCROLL_LOCK)
            combination.complexity += 30
            return true
        }
      })

      if (!shouldHandle) {
        return
      }

      rule.behaviors.forEach(function(behavior) {
        switch (behavior.type) {
          case 'literal':
            if (!charmap[behavior.value]) {
              charmap[behavior.value] = [combination]
            }
            else {
              charmap[behavior.value].push(combination)

              // Could be more efficient, but we only have 1-4 combinations
              // per key, so we don't really care.
              charmap[behavior.value].sort(function(a, b) {
                return a.complexity - b.complexity
              })
            }
            break
        }
      })
    })
  })

  return charmap
}
