text: import clone from 'clone';
  import equal from 'deep-equal';
  import extend from 'extend';
  import DeltaOp from 'quill-delta/lib/op';
  import Parchment from 'parchment';
  import Quill from '../core/quill';
  import logger from '../core/logger';
  import Module from '../core/module';
  
  let debug = logger('quill:keyboard');
  
  const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
  
  
  class Keyboard extends Module {
    static match(evt, binding) {
      binding = normalize(binding);
      if (!!binding.shortKey !== evt[SHORTKEY] && binding.shortKey !== null) return false;
      if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(function(key) {
        return (key != SHORTKEY && !!binding[key] !== evt[key] && binding[key] !== null);
      })) {
        return false;
      }
      return binding.key === (evt.which || evt.keyCode);
    }
  
    constructor(quill, options) {
      super(quill, options);
      this.bindings = {};
      Object.keys(this.options.bindings).forEach((name) => {
        if (this.options.bindings[name]) {
          this.addBinding(this.options.bindings[name]);
        }
      });
      this.addBinding({ key: Keyboard.keys.ENTER, shiftKey: null }, handleEnter);
      this.addBinding({ key: Keyboard.keys.ENTER, metaKey: null, ctrlKey: null, altKey: null }, function() {});
      if (/Firefox/i.test(navigator.userAgent)) {
        // Need to handle delete and backspace for Firefox in the general case #1171
        this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true }, handleBackspace);
        this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true }, handleDelete);
      } else {
        this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true, prefix: /^.?$/ }, handleBackspace);
        this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true, suffix: /^.?$/ }, handleDelete);
      }
      this.addBinding({ key: Keyboard.keys.BACKSPACE }, { ctrlKey: true }, function() {});
      this.addBinding({ key: Keyboard.keys.DELETE }, { ctrlKey: true }, function() {});
      this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: false }, handleDeleteRange);
      this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: false }, handleDeleteRange);
      this.addBinding({ key: Keyboard.keys.BACKSPACE }, { empty: true, shortKey: true }, handleBackspace);
      this.listen();
    }
  
    addBinding(key, context = {}, handler = {}) {
      let binding = normalize(key);
      if (binding == null || binding.key == null) {
        return debug.warn('Attempted to add invalid keyboard binding', binding);
      }
      if (typeof context === 'function') {
        context = { handler: context };
      }
      if (typeof handler === 'function') {
        handler = { handler: handler };
      }
      binding = extend(binding, context, handler);
      this.bindings[binding.key] = this.bindings[binding.key] || [];
      this.bindings[binding.key].push(binding);
    }
  
    listen() {
      this.quill.root.addEventListener('keydown', (evt) => {
        if (evt.defaultPrevented) return;
        let which = evt.which || evt.keyCode;
        let bindings = (this.bindings[which] || []).filter(function(binding) {
          return Keyboard.match(evt, binding);
        });
        if (bindings.length === 0) return;
        let range = this.quill.getSelection();
        if (range == null || !this.quill.hasFocus()) return;
        let [line, offset] = this.quill.getLine(range.index);
        let [leafStart, offsetStart] = this.quill.getLeaf(range.index);
        let [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length);
        let prefixText = leafStart instanceof Parchment.Text ? leafStart.value().slice(0, offsetStart) : '';
        let suffixText = leafEnd instanceof Parchment.Text ? leafEnd.value().slice(offsetEnd) : '';
        let curContext = {
          collapsed: range.length === 0,
          empty: range.length === 0 && line.length() <= 1,
          format: this.quill.getFormat(range),
          offset: offset,
          prefix: prefixText,
          suffix: suffixText
        };
        let prevented = bindings.some((binding) => {
          if (binding.collapsed != null && binding.collapsed !== curContext.collapsed) return false;
          if (binding.empty != null && binding.empty !== curContext.empty) return false;
          if (binding.offset != null && binding.offset !== curContext.offset) return false;
          if (Array.isArray(binding.format)) {
            // any format is present
            if (binding.format.every(function(name) {
              return curContext.format[name] == null;
            })) {
              return false;
            }
          } else if (typeof binding.format === 'object') {
            // all formats must match
            if (!Object.keys(binding.format).every(function(name) {
              if (binding.format[name] === true) return curContext.format[name] != null;
              if (binding.format[name] === false) return curContext.format[name] == null;
              return equal(binding.format[name], curContext.format[name]);
            })) {
              return false;
            }
          }
          if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) return false;
          if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) return false;
          return binding.handler.call(this, range, curContext) !== true;
        });
        if (prevented) {
          evt.preventDefault();
        }
      });
    }
  }
  
  Keyboard.keys = {
    BACKSPACE: 8,
    TAB: 9,
    ENTER: 13,
    ESCAPE: 27,
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40,
    DELETE: 46
  };
  
  Keyboard.DEFAULTS = {
    bindings: {
      'bold'      : makeFormatHandler('bold'),
      'italic'    : makeFormatHandler('italic'),
      'underline' : makeFormatHandler('underline'),
      'indent': {
        // highlight tab or tab at beginning of list, indent or blockquote
        key: Keyboard.keys.TAB,
        format: ['blockquote', 'indent', 'list'],
        handler: function(range, context) {
          if (context.collapsed && context.offset !== 0) return true;
          this.quill.format('indent', '+1', Quill.sources.USER);
        }
      },
      'outdent': {
        key: Keyboard.keys.TAB,
        shiftKey: true,
        format: ['blockquote', 'indent', 'list'],
        // highlight tab or tab at beginning of list, indent or blockquote
        handler: function(range, context) {
          if (context.collapsed && context.offset !== 0) return true;
          this.quill.format('indent', '-1', Quill.sources.USER);
        }
      },
      'outdent backspace': {
        key: Keyboard.keys.BACKSPACE,
        collapsed: true,
        format: ['blockquote', 'indent', 'list'],
        offset: 0,
        handler: function(range, context) {
          if (context.format.indent != null) {
            this.quill.format('indent', '-1', Quill.sources.USER);
          } else if (context.format.blockquote != null) {
            this.quill.format('blockquote', false, Quill.sources.USER);
          } else if (context.format.list != null) {
            this.quill.format('list', false, Quill.sources.USER);
          }
        }
      },
      'indent code-block': makeCodeBlockHandler(true),
      'outdent code-block': makeCodeBlockHandler(false),
      'remove tab': {
        key: Keyboard.keys.TAB,
        shiftKey: true,
        collapsed: true,
        prefix: /\t$/,
        handler: function(range) {
          this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
        }
      },
      'tab': {
        key: Keyboard.keys.TAB,
        handler: function(range, context) {
          if (!context.collapsed) {
            this.quill.scroll.deleteAt(range.index, range.length);
          }
          this.quill.insertText(range.index, '\t', Quill.sources.USER);
          this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
        }
      },
      'list empty enter': {
        key: Keyboard.keys.ENTER,
        collapsed: true,
        format: ['list'],
        empty: true,
        handler: function(range, context) {
          this.quill.format('list', false, Quill.sources.USER);
          if (context.format.indent) {
            this.quill.format('indent', false, Quill.sources.USER);
          }
        }
      },
      'checklist enter': {
        key: Keyboard.keys.ENTER,
        collapsed: true,
        format: { list: 'checked' },
        handler: function(range) {
          this.quill.scroll.insertAt(range.index, '\n');
          let [line, ] = this.quill.getLine(range.index + 1);
          line.format('list', 'unchecked');
          this.quill.update(Quill.sources.USER);
          this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
          this.quill.selection.scrollIntoView();
        }
      },
      'header enter': {
        key: Keyboard.keys.ENTER,
        collapsed: true,
        format: ['header'],
        suffix: /^$/,
        handler: function(range) {
          this.quill.scroll.insertAt(range.index, '\n');
          this.quill.formatText(range.index + 1, 1, 'header', false, Quill.sources.USER);
          this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
          this.quill.selection.scrollIntoView();
        }
      },
      'list autofill': {
        key: ' ',
        collapsed: true,
        format: { list: false },
        prefix: /^\s*?(1\.|-)$/,
        handler: function(range, context) {
          if (this.quill.scroll.whitelist != null && !this.quill.scroll.whitelist['list']) return true;
          let length = context.prefix.length;
          let value = context.prefix.trim().length === 1 ? 'bullet' : 'ordered'
          this.quill.scroll.deleteAt(range.index - length, length);
          this.quill.formatLine(range.index - length, 1, 'list', value, Quill.sources.USER);
          this.quill.setSelection(range.index - length, Quill.sources.SILENT);
        }
      },
      'code exit': {
        key: Keyboard.keys.ENTER,
        collapsed: true,
        format: ['code-block'],
        prefix: /\n\n$/,
        suffix: /^\s+$/,
        handler: function(range) {
          this.quill.format('code-block', false, Quill.sources.USER);
          this.quill.deleteText(range.index - 2, 1, Quill.sources.USER);
        }
      }
    }
  };
  
  
  function handleBackspace(range, context) {
    if (range.index === 0 || this.quill.getLength() <= 1) return;
    let [line, ] = this.quill.getLine(range.index);
    let formats = {};
    if (context.offset === 0) {
      let curFormats = line.formats();
      let prevFormats = this.quill.getFormat(range.index-1, 1);
      formats = DeltaOp.attributes.diff(curFormats, prevFormats) || {};
    }
    // Check for astral symbols
    let length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1;
    this.quill.deleteText(range.index-length, length, Quill.sources.USER);
    if (Object.keys(formats).length > 0) {
      this.quill.formatLine(range.index-length, length, formats, Quill.sources.USER);
    }
    this.quill.selection.scrollIntoView();
  }
  
  function handleDelete(range, context) {
    // Check for astral symbols
    let length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1;
    if (range.index >= this.quill.getLength() - length) return;
    this.quill.deleteText(range.index, length, Quill.sources.USER);
  }
  
  function handleDeleteRange(range) {
    this.quill.deleteText(range, Quill.sources.USER);
    this.quill.setSelection(range.index, Quill.sources.SILENT);
    this.quill.selection.scrollIntoView();
  }
  
  function handleEnter(range, context) {
    if (range.length > 0) {
      this.quill.scroll.deleteAt(range.index, range.length);  // So we do not trigger text-change
    }
    let lineFormats = Object.keys(context.format).reduce(function(lineFormats, format) {
      if (Parchment.query(format, Parchment.Scope.BLOCK) && !Array.isArray(context.format[format])) {
        lineFormats[format] = context.format[format];
      }
      return lineFormats;
    }, {});
    this.quill.insertText(range.index, '\n', lineFormats, Quill.sources.USER);
    // Earlier scroll.deleteAt might have messed up our selection,
    // so insertText's built in selection preservation is not reliable
    this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
    this.quill.selection.scrollIntoView();
    Object.keys(context.format).forEach((name) => {
      if (lineFormats[name] != null) return;
      if (Array.isArray(context.format[name])) return;
      if (name === 'link') return;
      this.quill.format(name, context.format[name], Quill.sources.USER);
    });
  }
  
  function makeCodeBlockHandler(indent) {
    return {
      key: Keyboard.keys.TAB,
      shiftKey: !indent,
      format: {'code-block': true },
      handler: function(range) {
        let CodeBlock = Parchment.query('code-block');
        let index = range.index, length = range.length;
        let [block, offset] = this.quill.scroll.descendant(CodeBlock, index);
        if (block == null) return;
        let scrollIndex = this.quill.getIndex(block);
        let start = block.newlineIndex(offset, true) + 1;
        let end = block.newlineIndex(scrollIndex + offset + length);
        let lines = block.domNode.textContent.slice(start, end).split('\n');
        offset = 0;
        lines.forEach((line, i) => {
          if (indent) {
            block.insertAt(start + offset, CodeBlock.TAB);
            offset += CodeBlock.TAB.length;
            if (i === 0) {
              index += CodeBlock.TAB.length;
            } else {
              length += CodeBlock.TAB.length;
            }
          } else if (line.startsWith(CodeBlock.TAB)) {
            block.deleteAt(start + offset, CodeBlock.TAB.length);
            offset -= CodeBlock.TAB.length;
            if (i === 0) {
              index -= CodeBlock.TAB.length;
            } else {
              length -= CodeBlock.TAB.length;
            }
          }
          offset += line.length + 1;
        });
        this.quill.update(Quill.sources.USER);
        this.quill.setSelection(index, length, Quill.sources.SILENT);
      }
    };
  }
  
  function makeFormatHandler(format) {
    return {
      key: format[0].toUpperCase(),
      shortKey: true,
      handler: function(range, context) {
        this.quill.format(format, !context.format[format], Quill.sources.USER);
      }
    };
  }
  
  function normalize(binding) {
    if (typeof binding === 'string' || typeof binding === 'number') {
      return normalize({ key: binding });
    }
    if (typeof binding === 'object') {
      binding = clone(binding, false);
    }
    if (typeof binding.key === 'string') {
      if (Keyboard.keys[binding.key.toUpperCase()] != null) {
        binding.key = Keyboard.keys[binding.key.toUpperCase()];
      } else if (binding.key.length === 1) {
        binding.key = binding.key.toUpperCase().charCodeAt(0);
      } else {
        return null;
      }
    }
    return binding;
  }
  
  
  export default Keyboard;
  