#!/usr/bin/python3

import argparse
import asyncio
import os
import sys
import threading
import timeit

import colorama
import pkg_resources

from klongpy import KlongInterpreter
from klongpy.core import kg_write

"""

    KlongPy REPL: See https://t3x.org/klong/klong-ref.txt.html for additional details.

"""

def sys_cmd_shell(klong, cmd):
    """

        ]! command                                               [Shell]

        Pass the given command to the Unix shell.

    """
    os.system(cmd[2:].strip())
    return None


def sys_cmd_apropos(klong, cmd):
    """

        ]a topic                                               [Apropos]

        ]htopic is short for help("topic"). In addition, ]hall will
        list all available help texts. The "topic" must be an operator
        symbol or operator name (e.g. :: or Define).

    """
    # TODO
    return None


def sys_cmd_help(klong, cmd):
    """

        ]h topic                                                  [Help]

        ]htopic is short for help("topic"). In addition, ]hall will
        list all available help texts. The "topic" must be an operator
        symbol or operator name (e.g. :: or Define).

    """
    # TODO
    return None


def sys_cmd_dir(klong, cmd):
    """

        ]i dir                                               [Inventory]

        List all *.kg files (Klong source programs) in the given
        directory. When no directory is given, it defaults to the first
        element of the KLONGPATH variable. The ]i command depends on a
        Unix shell and the "ls" utility (it does "cd dir; ls *.kg").

    """
    cmd = cmd[2:].strip()
    dir = cmd if cmd else (os.environ.get("KLONGPATH") or "./").split(":")[0]
    os.system(f"cd {dir}; ls *.kg")
    return None


def sys_cmd_load(klong, cmd):
    """

        ]l file                                                   [Load]

        ]lfile is short for .l("file").

    """
    klong(f'.l("{cmd[2:]}")')
    return None


def sys_cmd_exit(klong, cmd):
    """

        ]q                                                        [Exit]

        ]q is short for .x(0). However, end-of-file (control-D on Unix)
        typically also works.

    """
    print("bye!")
    sys.exit(0)


def sys_cmd_transcript(klong, cmd):
    """
        ]t file                                             [Transcript]

        Start appending user input and computed values to the given file.
        When no file is given, stop transcript. Input will be prefixed
        with a TAB (HT) character in the transcript file.

    """
    # TODO
    return None


def sys_cmd_timeit(klong, cmd):
    """
        ]T <prog>
        ]T:N <prog>

        Time an klong expression for 1 or N iterations using the timeit facility.

        As Klong manual shows, you can perform timing functions in Klong using the .pc() operator.

        timeit::{[t0];t0::.pc();x@[];.pc()-t0}

        and then use it for a nilad:

        timeit({1+1})

        For one iteration, it's possible this Klong timeit is more accurate than the native Python timeit due to overhead.

        Note: Added in KlongPy.

    """
    n = int(cmd[3:cmd.index(" ")]) if cmd[2] == ":" else 1
    r = timeit.timeit(lambda k=klong,p=cmd[cmd.index(" "):]: k(p), number=n)
    return f"total: {r} per: {r/n}"


def create_sys_cmd_functions():
    def _get_name(s):
        s = s.strip()
        x = s.index(']')+1
        return s[x:x+1]

    registry = {}

    m = sys.modules[__name__]
    for x in filter(lambda n: n.startswith("sys_cmd_"), dir(m)):
        fn = getattr(m,x)
        name = _get_name(fn.__doc__)
        registry[name] = fn 

    return registry


success = lambda input: f"{colorama.Fore.GREEN}{input}"
failure = lambda input: f"{colorama.Fore.RED}{input}"


def repl_eval(klong, p, verbose=True):
    try:
        r = klong(p)
        r = r if r is None else success(kg_write(r, display=True))
    except Exception as e:
        r = failure(f"Error: {e.args}")
        if verbose:
            import traceback
            traceback.print_exception(type(e), e, e.__traceback__)

    return r


def show_repl_header(ipc_addr=None):
    print()
    print(f"{colorama.Fore.GREEN}Welcome to KlongPy REPL v{pkg_resources.get_distribution('klongpy').version}")
    print(f"{colorama.Fore.GREEN}author: Brian Guarraci")
    print(f"{colorama.Fore.GREEN}repo  : https://github.com/briangu/klongpy")
    print(f"{colorama.Fore.YELLOW}crtl-d or ]q to quit")
    print()
    if ipc_addr:
        print(f"{colorama.Fore.RED}Running IPC server at {ipc_addr}")
        print()


def get_input():
    return input("?> ")


class ConsoleInputHandler:
    @staticmethod
    async def input_producer(loop, klong, verbose=False):
        sys_cmds = create_sys_cmd_functions()

        while True:
            try:
                s = await loop.run_in_executor(None, get_input)
                if len(s) == 0:
                    continue
                if s.startswith("]"):
                    if s[1] in sys_cmds:
                        r = sys_cmds[s[1]](klong, s)
                    else:
                        print(f"unkown system command: ]{s[1]}")
                        continue
                else:
                    r = repl_eval(klong, s, verbose=verbose)
                if r is not None:
                    print(r)
            except EOFError:
                print("\rbye!")
                loop.stop()
                break
            except KeyboardInterrupt:
                print(failure("\nkg: error: interrupted"))
            except Exception as e:
                print(failure(f"Error: {e.args}"))
                import traceback
                traceback.print_exc(e)


def run_file(klong, fname):
    with open(fname, "r") as f:
        return klong(f.read())


def start_io_loop(ioloop, exit_event):
    asyncio.set_event_loop(ioloop)
    ioloop.run_forever()
    exit_event.set()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        prog='KlongPy',
        description='KlongPy REPL',
        epilog='For help, go to https://github.com/briangu/klongpy')
    parser.add_argument('-e', '--expr', help='evaluate expression, no interactive mode')
    parser.add_argument('-l', '--load', help='load program from file')
    parser.add_argument('-s', '--server', help='start the IPC server', type=str)
    parser.add_argument('-t', '--test', help='test program from file')
    parser.add_argument('-v', '--verbose', help='enable verbose output', action="store_true")
    parser.add_argument('filename', nargs='?', help='filename to be run if no flags are specified')

    args = parser.parse_args()

    if args.expr:
        print(KlongInterpreter()(args.expr))
        exit()

    klong = KlongInterpreter()

    # Create and start the I/O loop on a separate thread
    io_loop = asyncio.new_event_loop()
    exit_event = threading.Event()
    io_thread = threading.Thread(target=start_io_loop, args=(io_loop,exit_event), daemon=True)
    io_thread.start()

    klong_loop = asyncio.new_event_loop()
    asyncio.set_event_loop(klong_loop)

    klong['.system'] = {'ioloop': io_loop, 'klongloop': klong_loop}
    klong['.os'] = {'environ': os.environ, 'argv': sys.argv}

    run_repl = False

    if args.server:
        r = klong(f".srv({args.server})")
        if r == 0:
            print(f"Failed to start server")
    elif args.test:
        print(f"Test: {args.test}")
        with open(args.test, "r") as f:
            for x in f.readlines():
                x = x.strip()
                if len(x) == 0 or x.startswith(":"):
                    continue
                print(x)
                klong(x)
    
    if args.filename:
        run_file(klong, args.filename)
    else:
        run_repl = True

    if run_repl:
        if args.load:
            print(f"Loading: {args.load}")
            with open(args.load, "r") as f:
                klong(f.read())
        colorama.init(autoreset=True)
        show_repl_header(args.server)
        klong_loop.create_task(ConsoleInputHandler.input_producer(klong_loop, klong, args.verbose))

    # TODO: we need to gracefully exit here if there are no more tasks
    #    running in the event loop.
    klong_loop.run_forever()
    # if asyncio.all_tasks(loop=klong_loop):
    #     klong_loop.run_forever()
    if asyncio.all_tasks(loop=io_loop):
        exit_event.wait()
