const DEFAULT_RELEASE_SEQUENCE = Object.freeze([27, 27, 27]);
const CLIPBOARD_COPY_SHORTCUT = 'clipboard-copy';
const CLIPBOARD_PASTE_SHORTCUT = 'clipboard-paste';

const KEY_LABELS = {
  8: 'Backspace',
  9: 'Tab',
  13: 'Enter',
  16: 'Shift',
  17: 'Control',
  18: 'Alt',
  27: 'Esc',
  32: 'Space',
  37: 'Left',
  38: 'Up',
  39: 'Right',
  40: 'Down',
  91: 'Command',
  93: 'Command',
  186: ';',
  187: '=',
  188: ',',
  189: '-',
  190: '.',
  191: '/',
  192: '`',
  219: '[',
  220: '\\',
  221: ']',
  222: "'",
};

const KEY_ALIASES = {
  alt: 18,
  option: 18,
  backspace: 8,
  cmd: 91,
  command: 91,
  control: 17,
  ctrl: 17,
  down: 40,
  arrowdown: 40,
  enter: 13,
  return: 13,
  esc: 27,
  escape: 27,
  left: 37,
  arrowleft: 37,
  right: 39,
  arrowright: 39,
  shift: 16,
  space: 32,
  spacebar: 32,
  tab: 9,
  up: 38,
  arrowup: 38,
};

for (let code = 65; code <= 90; code += 1) {
  const letter = String.fromCharCode(code);
  KEY_LABELS[code] = letter;
  KEY_ALIASES[letter.toLowerCase()] = code;
}

for (let code = 48; code <= 57; code += 1) {
  const number = String.fromCharCode(code);
  KEY_LABELS[code] = number;
  KEY_ALIASES[number] = code;
}

for (let index = 1; index <= 12; index += 1) {
  const code = 111 + index;
  KEY_LABELS[code] = `F${index}`;
  KEY_ALIASES[`f${index}`] = code;
}

function keyToken(token) {
  return String(token || '').trim().toLowerCase().replace(/[\s_-]+/g, '');
}

function parseKeyToken(token) {
  const normalized = keyToken(token);
  if (!normalized) {
    return null;
  }
  if (Object.prototype.hasOwnProperty.call(KEY_ALIASES, normalized)) {
    return KEY_ALIASES[normalized];
  }
  return null;
}

function parseKeySequence(value) {
  if (Array.isArray(value)) {
    return value
      .map(code => Number(code))
      .filter(code => Number.isInteger(code) && code > 0);
  }

  return String(value || '')
    .split(/[,+\s]+/)
    .map(parseKeyToken)
    .filter(code => code !== null);
}

function normalizeKeySequence(value, fallback) {
  const parsed = parseKeySequence(value);
  if (parsed.length) {
    return parsed;
  }
  return (fallback || DEFAULT_RELEASE_SEQUENCE).slice();
}

function keyCodeLabel(code) {
  return KEY_LABELS[code] || String(code);
}

function formatKeySequence(sequence) {
  return normalizeKeySequence(sequence).map(keyCodeLabel).join(', ');
}

function modifierNamesContain(modifierNames, name) {
  return (modifierNames || []).indexOf(name) !== -1;
}

function clipboardShortcutForCode(code, modifierNames) {
  if (!modifierNamesContain(modifierNames, 'command') ||
      !modifierNamesContain(modifierNames, 'alt')) {
    return null;
  }
  if (code === KEY_ALIASES.c) {
    return CLIPBOARD_COPY_SHORTCUT;
  }
  if (code === KEY_ALIASES.v) {
    return CLIPBOARD_PASTE_SHORTCUT;
  }
  return null;
}

function isClipboardTransferShortcut(name) {
  return name === CLIPBOARD_COPY_SHORTCUT || name === CLIPBOARD_PASTE_SHORTCUT;
}

function createSequenceMatcher(sequenceSource, options) {
  const timeoutMs = Number((options && options.timeoutMs) || 900);
  let progress = 0;
  let lastAt = 0;

  function sequence() {
    const source = typeof sequenceSource === 'function' ? sequenceSource() : sequenceSource;
    return normalizeKeySequence(source);
  }

  function reset() {
    progress = 0;
    lastAt = 0;
  }

  function push(code, now) {
    const current = sequence();
    const timestamp = Number(now || Date.now());

    if (!current.length) {
      reset();
      return { matched: false, consume: false, progress: 0 };
    }

    if (lastAt && timestamp - lastAt > timeoutMs) {
      progress = 0;
    }

    if (code === current[progress]) {
      progress += 1;
      lastAt = timestamp;
      if (progress >= current.length) {
        reset();
        return { matched: true, consume: true, progress: 0 };
      }
      return { matched: false, consume: true, progress };
    }

    progress = code === current[0] ? 1 : 0;
    lastAt = progress ? timestamp : 0;
    return { matched: false, consume: !!progress, progress };
  }

  return { push, reset };
}

module.exports = {
  CLIPBOARD_COPY_SHORTCUT,
  CLIPBOARD_PASTE_SHORTCUT,
  DEFAULT_RELEASE_SEQUENCE,
  clipboardShortcutForCode,
  createSequenceMatcher,
  formatKeySequence,
  isClipboardTransferShortcut,
  keyCodeLabel,
  normalizeKeySequence,
  parseKeySequence,
};
