import {
    select as d3_select,
} from 'd3-selection';

import { utilArrayUniq } from './array';

export interface Binding {
    id: string;
    event: {
        key: string | string[] | undefined;
        keyCode: number;
        modifiers: {
            shiftKey: boolean;
            ctrlKey: boolean;
            altKey: boolean;
            metaKey: boolean;
        };
    };
    capture: boolean;
    callback(event: KeyboardEvent): void;
}

export function utilKeybinding(namespace: string) {
    var _keybindings: Record<string, Binding> = {};


    function testBindings(d3_event: KeyboardEvent, isCapturing: boolean) {
        var didMatch = false;
        var bindings = Object.keys(_keybindings).map(function(id) { return _keybindings[id]; });

        // Most key shortcuts will accept either lower or uppercase ('h' or 'H'),
        // so we don't strictly match on the shift key, but we prioritize
        // shifted keybindings first, and fallback to unshifted only if no match.
        // (This lets us differentiate between '←'/'⇧←' or '⌘Z'/'⌘⇧Z')

        // priority match shifted keybindings first
        for (const binding of bindings) {
            if (!binding.event.modifiers.shiftKey) continue;  // no shift
            if (!!binding.capture !== isCapturing) continue;
            if (matches(d3_event, binding, true)) {
                binding.callback(d3_event);
                didMatch = true;

                // match a max of one binding per event
                break;
            }
        }

        if (didMatch) return;

        // then unshifted keybindings
        for (const binding of bindings) {
            if (binding.event.modifiers.shiftKey) continue;   // shift
            if (!!binding.capture !== isCapturing) continue;
            if (matches(d3_event, binding, false)) {
                binding.callback(d3_event);
                break;
            }
        }


        function matches(d3_event: KeyboardEvent, binding: Binding, testShift: boolean) {
            var event = d3_event;
            var isMatch = false;
            var tryKeyCode = true;

            // Prefer a match on `KeyboardEvent.key`
            if (event.key !== undefined) {
                tryKeyCode = (event.key.charCodeAt(0) > 127);  // outside ISO-Latin-1
                isMatch = true;

                if (binding.event.key === undefined) {
                    isMatch = false;
                } else if (Array.isArray(binding.event.key)) {
                    if (binding.event.key.map(function(s) {
                        return s.toLowerCase();
                    }).indexOf(event.key.toLowerCase()) === -1) {
                        isMatch = false;
                    }
                } else {
                    if (event.key.toLowerCase() !== binding.event.key.toLowerCase()) {
                        isMatch = false;
                    }
                }
            }

            // Fallback match on `KeyboardEvent.keyCode`, can happen if:
            // - `KeyboardEvent.key` is outside ASCII range (e.g. cyrillic - #  )
            // - alt/option/⌥ key is also requested (e.g. Spanish keyboard on MacOS - #8905)
            if (!isMatch && (tryKeyCode || binding.event.modifiers.altKey)) {
                isMatch = (event.keyCode === binding.event.keyCode);
            }

            if (!isMatch) return false;

            // test modifier keys
            if (!(event.ctrlKey && event.altKey)) {  // if both are set, assume AltGr and skip it - #4096
                if (event.ctrlKey !== binding.event.modifiers.ctrlKey) return false;
                if (event.altKey !== binding.event.modifiers.altKey) return false;
            }
            if (event.metaKey !== binding.event.modifiers.metaKey) return false;
            if (testShift && event.shiftKey !== binding.event.modifiers.shiftKey) return false;

            return true;
        }
    }


    function capture(d3_event: KeyboardEvent) {
        testBindings(d3_event, true);
    }


    function bubble(d3_event: KeyboardEvent) {
        var tagName = d3_select(d3_event.target as HTMLElement).node()!.tagName;
        if (tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA') {
            return;
        }
        testBindings(d3_event, false);
    }


    function keybinding(selection?: d3.Selection<Document>) {
        selection = selection || d3_select<Document, unknown>(document);
        selection.on('keydown.capture.' + namespace, capture, true);
        selection.on('keydown.bubble.' + namespace, bubble, false);
        return keybinding;
    }

    // was: keybinding.off()
    keybinding.unbind = function(selection?: d3.Selection<Document>) {
        _keybindings = {};
        selection = selection || d3_select<Document, unknown>(document);
        selection.on('keydown.capture.' + namespace, null);
        selection.on('keydown.bubble.' + namespace, null);
        return keybinding;
    };


    keybinding.clear = function() {
        _keybindings = {};
        return keybinding;
    };


    // Remove one or more keycode bindings.
    keybinding.off = function(codes: string | string[], capture: boolean) {
        var arr = utilArrayUniq(typeof codes === 'string' ? [codes] : codes);

        for (var i = 0; i < arr.length; i++) {
            var id = arr[i] + (capture ? '-capture' : '-bubble');
            delete _keybindings[id];
        }
        return keybinding;
    };


    // Add one or more keycode bindings.
    keybinding.on = function(codes: string | string[], callback: Binding['callback'], capture: boolean) {
        if (typeof callback !== 'function') {
            return keybinding.off(codes, capture);
        }

        var arr = utilArrayUniq(typeof codes === 'string' ? [codes] : codes);

        for (var i = 0; i < arr.length; i++) {
            var id = arr[i] + (capture ? '-capture' : '-bubble');
            var binding: Binding = {
                id: id,
                capture: capture,
                callback: callback,
                event: {
                    key: undefined,  // preferred
                    keyCode: 0,      // fallback
                    modifiers: {
                        shiftKey: false,
                        ctrlKey: false,
                        altKey: false,
                        metaKey: false
                    }
                }
            };

            if (_keybindings[id]) {
                console.warn('warning: duplicate keybinding for "' + id + '"'); // eslint-disable-line no-console
            }

            _keybindings[id] = binding;

            var matches = arr[i].toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g)!;
            for (var j = 0; j < matches.length; j++) {
                // Normalise matching errors
                if (matches[j] === '++') matches[j] = '+';

                if (matches[j] in utilKeybinding.modifierCodes) {
                    var prop = utilKeybinding.modifierProperties[utilKeybinding.modifierCodes[matches[j]]];
                    binding.event.modifiers[prop] = true;
                } else {
                    binding.event.key = utilKeybinding.keys[matches[j]] || matches[j];
                    if (matches[j] in utilKeybinding.keyCodes) {
                        binding.event.keyCode = utilKeybinding.keyCodes[matches[j]];
                    }
                }
            }
        }

        return keybinding;
    };


    return keybinding;
}


/*
 * See https://github.com/keithamus/jwerty
 */

utilKeybinding.modifierCodes = {
    // Shift key, ⇧
    '⇧': 16, shift: 16,
    // CTRL key, on Mac: ⌃
    '⌃': 17, ctrl: 17,
    // ALT key, on Mac: ⌥ (Alt)
    '⌥': 18, alt: 18, option: 18,
    // META, on Mac: ⌘ (CMD), on Windows (Win), on Linux (Super)
    '⌘': 91, meta: 91, cmd: 91, 'super': 91, win: 91
} as Record<string, number>;

utilKeybinding.modifierProperties = {
    16: 'shiftKey',
    17: 'ctrlKey',
    18: 'altKey',
    91: 'metaKey'
} as Record<number, keyof Binding['event']['modifiers']>;

utilKeybinding.plusKeys = ['plus', 'ffplus', '=', 'ffequals', '≠', '±'];
utilKeybinding.minusKeys = ['_', '-', 'ffminus', 'dash', '–', '—'];

/* eslint-disable sort-keys */
utilKeybinding.keys = {
    // Backspace key, on Mac: ⌫ (Backspace)
    '⌫': 'Backspace', backspace: 'Backspace',
    // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥
    '⇥': 'Tab', '⇆': 'Tab', tab: 'Tab',
    // Return key, ↩
    '↩': 'Enter', '↵': 'Enter', '⏎': 'Enter', 'return': 'Enter', enter: 'Enter', '⌅': 'Enter',
    // Pause/Break key
    'pause': 'Pause', 'pause-break': 'Pause',
    // Caps Lock key, ⇪
    '⇪': 'CapsLock', caps: 'CapsLock', 'caps-lock': 'CapsLock',
    // Escape key, on Mac: ⎋, on Windows: Esc
    '⎋': ['Escape', 'Esc'], escape: ['Escape', 'Esc'], esc: ['Escape', 'Esc'],
    // Space key
    space: [' ', 'Spacebar'],
    // Page-Up key, or pgup, on Mac: ↖
    '↖': 'PageUp', pgup: 'PageUp', 'page-up': 'PageUp',
    // Page-Down key, or pgdown, on Mac: ↘
    '↘': 'PageDown', pgdown: 'PageDown', 'page-down': 'PageDown',
    // END key, on Mac: ⇟
    '⇟': 'End', end: 'End',
    // HOME key, on Mac: ⇞
    '⇞': 'Home', home: 'Home',
    // Insert key, or ins
    ins: 'Insert', insert: 'Insert',
    // Delete key, on Mac: ⌦ (Delete)
    '⌦': ['Delete', 'Del'], del: ['Delete', 'Del'], 'delete': ['Delete', 'Del'],
    // Left Arrow Key, or ←
    '←': ['ArrowLeft', 'Left'], left: ['ArrowLeft', 'Left'], 'arrow-left': ['ArrowLeft', 'Left'],
    // Up Arrow Key, or ↑
    '↑': ['ArrowUp', 'Up'], up: ['ArrowUp', 'Up'], 'arrow-up': ['ArrowUp', 'Up'],
    // Right Arrow Key, or →
    '→': ['ArrowRight', 'Right'], right: ['ArrowRight', 'Right'], 'arrow-right': ['ArrowRight', 'Right'],
    // Up Arrow Key, or ↓
    '↓': ['ArrowDown', 'Down'], down: ['ArrowDown', 'Down'], 'arrow-down': ['ArrowDown', 'Down'],
    // odities, stuff for backward compatibility (browsers and code):
    // Num-Multiply, or *
    '*': ['*', 'Multiply'], star: ['*', 'Multiply'], asterisk: ['*', 'Multiply'], multiply: ['*', 'Multiply'],
    // Num-Plus or +
    '+': ['+', 'Add'], 'plus': ['+', 'Add'],
    // Num-Subtract, or -
    '-': ['-', 'Subtract'], subtract: ['-', 'Subtract'], 'dash': ['-', 'Subtract'],
    // Semicolon
    semicolon: ';',
    // = or equals
    equals: '=',
    // Comma, or ,
    comma: ',',
    // Period, or ., or full-stop
    period: '.', 'full-stop': '.',
    // Slash, or /, or forward-slash
    slash: '/', 'forward-slash': '/',
    // Tick, or `, or back-quote
    tick: '`', 'back-quote': '`',
    // Open bracket, or [
    'open-bracket': '[',
    // Back slash, or \
    'back-slash': '\\',
    // Close bracket, or ]
    'close-bracket': ']',
    // Apostrophe, or Quote, or '
    quote: '\'', apostrophe: '\'',
    // NUMPAD 0-9
    'num-0': '0',
    'num-1': '1',
    'num-2': '2',
    'num-3': '3',
    'num-4': '4',
    'num-5': '5',
    'num-6': '6',
    'num-7': '7',
    'num-8': '8',
    'num-9': '9',
    // F1-F25
    f1: 'F1',
    f2: 'F2',
    f3: 'F3',
    f4: 'F4',
    f5: 'F5',
    f6: 'F6',
    f7: 'F7',
    f8: 'F8',
    f9: 'F9',
    f10: 'F10',
    f11: 'F11',
    f12: 'F12',
    f13: 'F13',
    f14: 'F14',
    f15: 'F15',
    f16: 'F16',
    f17: 'F17',
    f18: 'F18',
    f19: 'F19',
    f20: 'F20',
    f21: 'F21',
    f22: 'F22',
    f23: 'F23',
    f24: 'F24',
    f25: 'F25'
} as Record<string, string | string[]>;

utilKeybinding.keyCodes = {
    // Backspace key, on Mac: ⌫ (Backspace)
    '⌫': 8, backspace: 8,
    // Tab Key, on Mac: ⇥ (Tab), on Windows ⇥⇥
    '⇥': 9, '⇆': 9, tab: 9,
    // Return key, ↩
    '↩': 13, '↵': 13, '⏎': 13, 'return': 13, enter: 13, '⌅': 13,
    // Pause/Break key
    'pause': 19, 'pause-break': 19,
    // Caps Lock key, ⇪
    '⇪': 20, caps: 20, 'caps-lock': 20,
    // Escape key, on Mac: ⎋, on Windows: Esc
    '⎋': 27, escape: 27, esc: 27,
    // Space key
    space: 32,
    // Page-Up key, or pgup, on Mac: ↖
    '↖': 33, pgup: 33, 'page-up': 33,
    // Page-Down key, or pgdown, on Mac: ↘
    '↘': 34, pgdown: 34, 'page-down': 34,
    // END key, on Mac: ⇟
    '⇟': 35, end: 35,
    // HOME key, on Mac: ⇞
    '⇞': 36, home: 36,
    // Insert key, or ins
    ins: 45, insert: 45,
    // Delete key, on Mac: ⌦ (Delete)
    '⌦': 46, del: 46, 'delete': 46,
    // Left Arrow Key, or ←
    '←': 37, left: 37, 'arrow-left': 37,
    // Up Arrow Key, or ↑
    '↑': 38, up: 38, 'arrow-up': 38,
    // Right Arrow Key, or →
    '→': 39, right: 39, 'arrow-right': 39,
    // Up Arrow Key, or ↓
    '↓': 40, down: 40, 'arrow-down': 40,
    // odities, printing characters that come out wrong:
    // Firefox Equals
    'ffequals': 61,
    // Num-Multiply, or *
    '*': 106, star: 106, asterisk: 106, multiply: 106,
    // Num-Plus or +
    '+': 107, 'plus': 107,
    // Num-Subtract, or -
    '-': 109, subtract: 109,
    // Vertical Bar / Pipe
    '|': 124,
    // Firefox Plus
    'ffplus': 171,
    // Firefox Minus
    'ffminus': 173,
    // Semicolon
    ';': 186, semicolon: 186,
    // = or equals
    '=': 187, 'equals': 187,
    // Comma, or ,
    ',': 188, comma: 188,
    // Dash / Underscore key
    'dash': 189,
    // Period, or ., or full-stop
    '.': 190, period: 190, 'full-stop': 190,
    // Slash, or /, or forward-slash
    '/': 191, slash: 191, 'forward-slash': 191,
    // Tick, or `, or back-quote
    '`': 192, tick: 192, 'back-quote': 192,
    // Open bracket, or [
    '[': 219, 'open-bracket': 219,
    // Back slash, or \
    '\\': 220, 'back-slash': 220,
    // Close bracket, or ]
    ']': 221, 'close-bracket': 221,
    // Apostrophe, or Quote, or '
    '\'': 222, quote: 222, apostrophe: 222
} as Record<string, number>;

// NUMPAD 0-9
var i = 95, n = 0;
while (++i < 106) {
    utilKeybinding.keyCodes['num-' + n] = i;
    ++n;
}

// 0-9
i = 47; n = 0;
while (++i < 58) {
    utilKeybinding.keyCodes[n] = i;
    ++n;
}

// F1-F25
i = 111; n = 1;
while (++i < 136) {
    utilKeybinding.keyCodes['f' + n] = i;
    ++n;
}

// a-z
i = 64;
while (++i < 91) {
    utilKeybinding.keyCodes[String.fromCharCode(i).toLowerCase()] = i;
}
/* eslint-enable sort-keys */
