#!/usr/bin/env python3


# TODO:
# - `ACTION_OVERRIDES` (`diw` as `lbcw\\e` etc).
# - `ciw` and `ysiw)` should be undone with a single `u`. Can this be done by
#   deleting and `vi-put`?
# - Counts.
# - Remove insert mode bindings and `bind_command`?


import sys
import os
import string
import itertools


## Bindings


def bind_macro(keyseq, macro):
    keyseq, macro = escape([keyseq, macro])
    print(f'"{keyseq}": "{macro}"')


def bind_command(keyseq, command):
    keyseq, = escape([keyseq])
    print(f'"{keyseq}": {command}')


def escape(strings):
    return [string.replace('"', '\\"') for string in strings]


def fmt(vars, strings):
    return [string.format(**vars) for string in strings]


## Insert mode


# print('set keymap vi-insert')


# PUT = '\C-r"'
# UNDO_BREAK = '\C-gu'  # TODO: Use.

# bind_command(PUT, '\\ePa')


## Command mode


print('set keymap vi-command')


# Change casing.
for op, case in [
    ['gu', 'downcase'],
    ['gU', 'upcase'],
]:
    for mov in ['w', 'e']:
        bind_command(f'{op}{mov}', f'{case}-word')
    bind_macro(f'{op}iw', f'lbg{op}w')


def movop(len, op=''):
    if len == 0:
        return ''
    dir = ['h', 'l'][len > 0]
    count = abs(len)
    if count == 1:
        count = ''
    return f'{count}{op}{dir}'


### Motions
# motion = mot, movs = mot, [mov]


MOTIONS_VI_WORDS = [
    'w', 'e', 'b',
]
MOTIONS_VI_OTHER = [
    'h', 'l',
    '0', '^', '$',
    # 'g0', 'g^', 'g$',  # TODO: Where did I get these from? Do they belong in
    # `MOTIONS_VIM_OTHER`?
    '|',
    ';', ',',
    '%',
]
MOTIONS_VIM_WORDS = [
    ['ge', 'helbbhe'],
]
MOTIONS_VIM_OTHER = [
    # ['_',  '^'],
    ['g_', '$bhe'],
    ['(',  '2F.w'],
    [')',  'f.w'],
]


def motions_unpack(motions):
    for motion in motions:
        if isinstance(motion, str):
            mot, mov = motion, motion
        else:
            mot, mov = motion
        yield mot, [mov]


def motions_unpack_words(motions):
    for mot, movs in motions_unpack(motions):
        yield mot, movs
        mot, *movs = mots_upper([mot, *movs])
        yield mot, movs


def mots_upper(mots):
    for lower, *_ in motions_unpack(MOTIONS_VI_WORDS):
        upper = lower.upper()
        mots = [mot.replace(lower, upper) for mot in mots]
    return mots


for mot, [mov] in [
    *motions_unpack([
        *MOTIONS_VIM_OTHER,
    ]),
    *motions_unpack_words([
        *MOTIONS_VIM_WORDS,
    ]),
]:
    bind_macro(mot, mov)


### Pairs
# pair = sides, distincts = [sidea, sideb], [sidea?, sideb?, alt?]


PAIRS_DISTINCT_MATCH = [
    [['(', ')'], 'b'],
    [['{', '}'], 'B'],
    [['[', ']']],
]
PAIRS_DISTINCT_OTHER = [
    [['<', '>']],
]
PAIRS_NONDISTINCT_QUOTES = [
    '"', '\'', '`',
]
PAIRS_NONDISTINCT_PUNCT = [
    [punct, '\\\\'][punct == '\\']
    for punct in [*string.punctuation, ' ']
    if punct not in itertools.chain(
        *[sides for sides, *_ in [
            *PAIRS_DISTINCT_MATCH,
            *PAIRS_DISTINCT_OTHER,
        ]],
        PAIRS_NONDISTINCT_QUOTES,
    )
]
PAIRS_MATCH = [
    *PAIRS_DISTINCT_MATCH,
]
PAIRS_NONMATCH = [
    *PAIRS_DISTINCT_OTHER,
    *PAIRS_NONDISTINCT_QUOTES,
    *PAIRS_NONDISTINCT_PUNCT,
]
PAIRS_ALL = [
    *PAIRS_MATCH,
    *PAIRS_NONMATCH,
]


def pairs_unpack(pairs):
    for pair in pairs:
        if isinstance(pair, str):
            sides, distincts = [pair, pair], [pair]
        else:
            sides, *alt = pair
            distincts = [*sides, *alt]
        yield sides, distincts


def pairs_classify(char):
    char_cls = ''
    for sides, *_ in pairs_unpack(PAIRS_ALL):
        for side, cls in zip(sides, ['(', ')']):
            if char == side:
                char_cls += cls
    if len(char_cls) > 1:
        char_cls = '*'
    return char_cls


## Text objects
# textobj = viobj, pref, obj, movs = viobj, pref, obj, [mova, movb]


def textobjs_motions(viobj, motions):
    for obj, [movb] in motions:
        yield viobj, '', obj, ['', movb]


def textobj_vi_motions_words():
    return textobjs_motions(True, motions_unpack_words(MOTIONS_VI_WORDS))


def textobj_vi_motions_other():
    return textobjs_motions(True, motions_unpack(MOTIONS_VI_OTHER))


def textobj_vim_motions_words():
    return textobjs_motions(False, motions_unpack_words(MOTIONS_VIM_WORDS))


def textobj_vim_motions_other():
    return textobjs_motions(False, motions_unpack(MOTIONS_VIM_OTHER))


def textobj_implicit_line():
    yield True, '', '', ['0', '$']


def textobj_vim_words():
    for pref, movs in [
        ['a', ['lb', 'wh']],
        ['i', ['lb', 'he']],
    ]:
        for obj in ['w', 'W']:
            yield False, pref, obj, [movs, mots_upper(movs)][obj.isupper()]


def textobj_vim_pairs():
    for pref in ['a', 'i']:
        for pairs, movs in [
            [PAIRS_MATCH,    ['lF{open}', '%'       ]],
            [PAIRS_NONMATCH, ['lF{open}', 'f{close}']],
        ]:
            for [open, close], objs in pairs_unpack(pairs):
                mova, movb = fmt(vars(), movs)
                if pref == 'i':
                    mova, movb = f'{mova}l', f'h{movb}h'
                for obj in objs:
                    yield False, pref, obj, [mova, movb]


## Operators
# operator = viop, op, subj, acts, off


OPERATORS_VI = [
    ['d', None],
    ['c', None],
    ['y', 0 ],
]


def operators_vi():
    for op, off in OPERATORS_VI:
        def operator(textobj, op=op, off=off):
            return textobj, True, op, '', None, off
        yield operator


def operators_vim_surround():
    def operator_s(textobj, op, subj, news):
        if op == 'y':
            oldlen = 0
            acta, actb = 'i', 'a'
        else:
            viobj, pref, obj, movs = textobj
            cls = pairs_classify(obj)
            if cls == '' or pref != 'a':
                return None
            textobj = viobj, '', obj, movs
            oldlen = [1, 2][cls == '(']
            fwd = movop(oldlen, op)
            bck = movop(1-oldlen)
            acta, actb = f'{fwd}', f'{bck}{fwd}'
        newa, newb = news
        newlen = len(newa)
        if op != 'd':
            acta, actb = f'{acta}{newa}\\e', f'{actb}{newb}\\e'
        return textobj, False, f'{op}s', subj, [acta, actb], newlen - oldlen

    # ds - delete surroundings
    def operator_ds(textobj):
        return operator_s(textobj, 'd', '', ['', ''])
    yield operator_ds

    for [open, close], subjs in pairs_unpack(PAIRS_ALL):
        for subj in subjs:
            space = ['', ' '][pairs_classify(subj) == '(']
            news = f'{open}{space}', f'{space}{close}'

            # cs - change surroundings
            def operator_cs(textobj, subj=subj, news=news):
                return operator_s(textobj, 'c', subj, news)
            yield operator_cs

            # ys - add surroundings ("you surround")
            def operator_ys(textobj, subj=subj, news=news):
                return operator_s(textobj, 'y', subj, news)
            yield operator_ys


## Actions
# action = operator(textobj) = textobj, *operator


def action(operator, textobj):
        # TODO: Special-case word/WORD macro objects, e.g. make `ciw` to use
        # something like `lbcw`. Use regex?
        action = operator(textobj)
        if not action:
            return
        textobj, *operator = action
        viop, op, subj, acts, off = operator
        viobj, pref, obj, movs = textobj
        if viop and viobj:
            return
        mova, movb = movs
        if not obj:
            obj = op[-1]
        if not acts:
            # TODO: This heuristic works today but is very ugly and is not
            # guaranteed to work tomorrow. E.g. counts. Maybe use a regex here?
            if len(movb.replace('f', '').replace('`', '')) <= 1:
                macro = f'{mova}{op}{movb}'
            else:
                macro = f'{mova}ma{movb}lmb`a{op}`b'
        else:
            acta, actb = acts
            macro = f'{mova}ma{movb}{actb}`a{acta}'
        if off is not None:
            macro = f'mc{macro}`c{movop(off)}'
        bind_macro(f'{op}{pref}{obj}{subj}', macro)


for operator in [
    *operators_vi(),
    *operators_vim_surround(),
]:
    for textobj in [
        *textobj_vi_motions_words(),
        *textobj_vi_motions_other(),
        *textobj_vim_motions_words(),
        *textobj_vim_motions_other(),
        *textobj_vim_words(),
        *textobj_vim_pairs(),
        *textobj_implicit_line(),
    ]:
        action(operator, textobj)
