#!/usr/bin/env python3
from RuneSolver import VigenereSolver, AffineSolver, AutokeySolver
from RuneText import alphabet, RuneText
import lib as LIB

SOLVER = VigenereSolver()
# SOLVER = AffineSolver()
# SOLVER = AutokeySolver()
SOLVER.input.load(file='_input.txt')


def main():
    help_str = '''
Welcome pilgrim,

Available commands are:
 a : Generate all 29 rotations of a given rune-word (a) or inverted rune (ai)
 d : Get decryption key (substitution) for a single phrase
 f : Find words with a given length (f 4, or f word)
 g : Print Gematria Primus (gp) or reversed Gematria (gpr)
 h : Highlight occurrences of interrupt jumps (hj) or reset (h)
 k : Re/set decryption key (k), invert key (ki),
   ': change key shift (ks), rotation (kr), offset (ko), or after padding (kp)
   ': set key jumps (kj) e.g., [1,2] (first appearence of ᚠ is index 1)
 l : Toggle log level: normal (ln), quiet (lq) verbose (lv)
 p : Prime number and emirp check
 t : Translate between runes, text, and indices (0-28)
 x : Execute decryption. Also: load data into memory
   ': set manually (x DATA) or load from file (xf p0-2) (default: _input.txt)
   ': limit/trim loaded data up to nth character (xl 300)

 ? : Show currently set variables
 q : Quit
'''
    print(help_str)
    while True:
        try:
            inpt = input('$>: ').strip()
            if not inpt:
                continue
            cmd_p = inpt.split(' ', 1)
            cmd = cmd_p[0].strip().lower()
            args = cmd_p[1].strip() if len(cmd_p) > 1 else ''

            if cmd[0] != 'l':  # only log mode allowed after find operation
                SOLVER.reset_highlight()

            if cmd == 'help':
                print(help_str)
            elif cmd == 'q' or cmd == 'exit' or cmd == 'quit':
                exit()
            elif cmd == '?':
                print(SOLVER)
            else:
                cmdX = {'a': command_a, 'd': command_d, 'f': command_f,
                        'g': command_g, 'h': command_h, 'k': command_k,
                        'l': command_l, 'p': command_p, 't': command_t,
                        'x': command_x}
                res = cmdX[cmd[0]](cmd, args) if cmd[0] in cmdX else False
                if res is False:
                    print('Command not found.')
        except Exception as e:
            print(e)


# this allow you to write `ks3` instead of `ks 3`
def get_cmd_int(cmd, args, desc=None, start=2):
    val = int(args) if args else 0
    if not val and cmd[start:]:
        val = int(cmd[start:])
    if desc:
        print(f'set {desc}: {val}')
    return val


#########################################
#                  A
#########################################

def command_a(cmd, args):  # [a]ll variations
    if cmd.strip('aliq'):
        return False
    if not args:
        raise Warning('No input provided.')
    root = RuneText(args)
    inclIndex = 'q' not in cmd
    if 'i' in cmd:
        root = ~root
    for i in range(29):
        print('{:02d}: {}'.format(i, (root + i).description(index=inclIndex)))


#########################################
#                  D
#########################################

def command_d(cmd, args):  # [d]ecrypt or single substitution
    if not (cmd in 'decrypt'):
        return False
    if not args:
        trythis = input('What is the encrypted text?: ').strip()
    else:
        trythis = args
    enc = RuneText(trythis)
    print('encrypted:', enc.description())
    target = input('What should the decrypted clear text be?: ').strip()
    plain = RuneText(target)
    print('plaintext:', plain.description())
    if len(enc) != len(plain):
        print('Error: key length mismatch')
    else:
        print('Substition:')
        print((enc.zip_sub(plain)).description())


#########################################
#                  F
#########################################

def command_f(cmd, args):  # (f)ind word
    if not (cmd in 'find'):
        return False
    if not args:
        raise Warning('No input provided.')
    try:
        search_term = None
        s_len = int(args.strip())  # search words with n-length
    except ValueError:
        search_term = RuneText(args)
        s_len = len(search_term)

    cur_words = SOLVER.highlight_words_with_len(s_len)
    SOLVER.run()
    print()
    print('Found:')
    for _, _, pos, _, w in cur_words:
        print(f'{pos:04}: {w.description(count=True)}')
    if search_term:
        print()
        keylen = [len(search_term)]
        if SOLVER.substitute_supports_keylen():
            try:
                inp = input('What is the key length? (num or [a]ll): ').strip()
                if inp:
                    if inp[0] == 'a':
                        keylen = range(len(search_term), 24)
                    else:
                        keylen = [int(inp)]
            except ValueError:
                raise ValueError('not a number.')
        print()
        print('Available substition:')
        for _, _, pos, _, w in cur_words:
            for kl in keylen:
                res = SOLVER.substitute_get(pos, kl, search_term, w)
                print(f'{pos:04}: {res}')


#########################################
#                  G
#########################################

def command_g(cmd, args):  # (g)ematria primus
    if cmd not in 'gp gpr gpi':
        return False

    def pp(i):
        p, r, t = alphabet[28 - i if rev else i]
        return '{:2d}  {} {:3d}  {}'.format(i, r, p, '/'.join(t))

    rev = cmd[-1] in 'ir' or len(args) > 0 and args[0] in 'ir'
    print('Gematria Primus (reversed)' if rev else 'Gematria Primus')
    half = (len(alphabet) >> 1) + (len(alphabet) & 1)  # ceil
    for i in range(half):
        if i < len(alphabet) // 2:
            print('{:22} {}'.format(pp(i), pp(i + half)))
        else:
            print(pp(i))


#########################################
#                  H
#########################################

def command_h(cmd, args):  # (h)ighlight
    if cmd == 'h':
        SOLVER.reset_highlight()
        SOLVER.run()
    elif cmd in 'hj hi':
        res = SOLVER.highlight_interrupt()
        SOLVER.run()
        txt = ''
        bits = ''
        # first appearance of ᚠ is l_pos == 1; r_pos is the index on runes only
        for l_pos, r_pos, _, is_set in res:
            txt += '{}.{}.{}  '.format(l_pos, 'T' if is_set else 'F', r_pos)
            bits += '1' if is_set else '0'
        print(f'\nInterrupts: {bits}\n{txt}')
    else:
        return False


#########################################
#                  K
#########################################

def command_k(cmd, args):  # (k)ey manipulation
    if cmd == 'k' or cmd == 'key':
        SOLVER.KEY_DATA = [x.index for x in RuneText(args)]
        print('set key: {}'.format(SOLVER.KEY_DATA))
    elif cmd[1] == 's':
        SOLVER.KEY_SHIFT = get_cmd_int(cmd, args, 'shift')
    elif cmd[1] == 'r':
        SOLVER.KEY_ROTATE = get_cmd_int(cmd, args, 'rotation')
    elif cmd[1] == 'o':
        SOLVER.KEY_OFFSET = get_cmd_int(cmd, args, 'offset')
    elif cmd[1] == 'p':
        SOLVER.KEY_POST_PAD = get_cmd_int(cmd, args, 'post padding')
    elif cmd[1] == 'i':
        SOLVER.KEY_INVERT = not SOLVER.KEY_INVERT
        print('set key invert: {}'.format(SOLVER.KEY_INVERT))
    elif cmd == 'kj':
        args = args.strip('[]')
        pos = [int(x) for x in args.split(',')] if args else []
        SOLVER.INTERRUPT_POS = pos
        print('set interrupt jumps: {}'.format(pos))
    else:
        return False  # command not found
    SOLVER.run()


#########################################
#                  L
#########################################

def command_l(cmd, args):  # (l)og level
    if cmd == 'lv' or args == 'v' or args == 'verbose':
        SOLVER.output.VERBOSE = not SOLVER.output.VERBOSE
    elif cmd == 'lq' or args == 'q' or args == 'quiet':
        SOLVER.output.QUIET = not SOLVER.output.QUIET
    elif cmd == 'ln' or args == 'n' or args == 'normal':
        SOLVER.output.VERBOSE = False
        SOLVER.output.QUIET = False
    else:
        return False
    SOLVER.run()


#########################################
#                  P
#########################################

def command_p(cmd, args):  # (p)rime test
    if args and cmd not in 'prime':
        return False
    p = get_cmd_int(cmd, args, start=1)
    print(p, ':', LIB.is_prime(p))
    print(LIB.rev(p), ':', LIB.is_prime(LIB.rev(p)))


#########################################
#                  T
#########################################

def command_t(cmd, args):  # (t)ranslate
    if cmd != 't':
        return False
    word = RuneText(args)
    x = word.as_dict()
    psum = sum(x['p'])
    sffx = '*' if LIB.is_prime(psum) else ''
    if LIB.is_prime(LIB.rev(psum)):
        sffx += '√'
    print('runes({}): {}'.format(len(x['r']), x['r']))
    print('plain({}): {}'.format(len(x['t']), x['t']))
    print('reversed: {}'.format(''.join([x.rune for x in ~word])))
    print('indices: {}'.format(x['i']))
    print('prime({}{}): {}'.format(psum, sffx, x['p']))


#########################################
#                  X
#########################################

def command_x(cmd, args):  # e(x)ecute decryption
    if cmd == 'x':
        pass  # just run the solver
    elif cmd == 'xf':  # reload from file
        file = f'pages/{args}.txt' if args else '_input.txt'
        print('loading file:', file)
        SOLVER.input.load(file=file)
        args = None  # so run() won't override data
    elif len(cmd) > 0 and cmd[1] == 'l':  # limit content
        limit = get_cmd_int(cmd, args, 'read limit')
        last_file = SOLVER.input.loaded_file
        if last_file:
            SOLVER.input.load(file=last_file)
        if limit > 0:
            SOLVER.input.data.trim(limit)
        args = None
    else:
        return False
    SOLVER.run(args if args else None)


if __name__ == '__main__':
    main()
