#!python

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

import colorama
import pkg_resources

from klongpy import KlongInterpreter
from klongpy.core import kg_write
from klongpy.utils import CallbackEvent

"""

    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()

        try:
            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!")
                    break
                except KeyboardInterrupt:
                    print(failure("\nkg: error: interrupted"))
                except Exception as e:
                    print(failure(f"Error: {e.args}"))
                    import traceback
                    traceback.print_exc()
        finally:
            while len(asyncio.all_tasks(loop=klong_loop)) > 1:
                print(asyncio.all_tasks(loop=klong_loop))
                time.sleep(0.1)

            for task in asyncio.all_tasks(loop=loop):
                task.cancel()
                
            loop.stop()


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


def start_io_loop(ioloop):
    asyncio.set_event_loop(ioloop)
    ioloop.run_forever()


if __name__ == "__main__":
    if '--' in sys.argv:
        index = sys.argv.index('--')
        main_args = sys.argv[:index]
        extras = sys.argv[index+1:]
    else:
        main_args = sys.argv
        extras = []

    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('-d', '--debug', help='enable debug mode', action="store_false")
    parser.add_argument('filename', nargs='?', help='filename to be run if no flags are specified')

    args = parser.parse_args(main_args[1:])

    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()
    if args.debug:
        io_loop.set_debug(True)
    io_thread = threading.Thread(target=start_io_loop, args=(io_loop,), daemon=True)
    io_thread.start()

    klong_loop = asyncio.new_event_loop()
    if args.debug:
        klong_loop.set_debug(True)
    asyncio.set_event_loop(klong_loop)

    shutdown_event = CallbackEvent()

    klong['.system'] = {'ioloop': io_loop, 'klongloop': klong_loop, 'closeEvent': shutdown_event}
    klong['.os.env'] = dict(os.environ)
    klong['.os.argv'] = extras if extras else []

    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))

    klong_loop.run_forever()
    klong_loop.close()

    shutdown_event.trigger()

    while len(asyncio.all_tasks(loop=io_loop)) > 0:
        if args.debug:
            print(asyncio.all_tasks(loop=io_loop))
        time.sleep(0.1)
        
    for task in asyncio.all_tasks(loop=io_loop):
        io_loop.call_soon_threadsafe(task.cancel)

    io_loop.call_soon_threadsafe(io_loop.stop)
    io_thread.join()
    io_loop.close()
