#!/usr/bin/python3

import argparse
import asyncio
import importlib.metadata
import os
import sys
import threading
import time
import timeit

import colorama

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

import importlib.resources

"""

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

    """
    try:
        klong['help']
    except KeyError:
        klong('.l("help.kg")')
    topic = cmd[2:].strip() if len(cmd) > 2 else ""
    klong(f'help("{topic}")')
    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]
    found = False
    for x in os.listdir(dir):
        if x.endswith(".kg"):
            print(x)
            found = True
    if not found:
        print(f"no .kg files found in {dir}")
    return None


def sys_cmd_load(klong, cmd):
    """

        ]l file                                                   [Load]

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

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


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}"


async def repl_eval(klong, p, verbose=True):
    try:
        r = klong(p)
        r = r if r is None else success(kg_write(r, display=False))
    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{importlib.metadata.distribution('klongpy').version}")
    print(f"{colorama.Fore.GREEN}Author: Brian Guarraci")
    print(f"{colorama.Fore.GREEN}Web: http://klongpy.org")
    print(f"{colorama.Fore.YELLOW}]h for help; 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("?> ")


def run_in_loop(klong_loop, coro):
    return asyncio.run_coroutine_threadsafe(coro, klong_loop).result()


class ConsoleInputHandler:
    @staticmethod
    async def input_producer(console_loop, klong_loop, klong, verbose=False):
        sys_cmds = create_sys_cmd_functions()
        try:
            while True:
                try:
                    s = await console_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 = run_in_loop(klong_loop, 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_exception(type(e), e, e.__traceback__)
        finally:
            console_loop.stop()


async def run_in_klong(klong, s):
    return klong(s)


def run_file(klong_loop, klong, fname, verbose=False):
    with open(fname, "r") as f:
        content = f.read()
    run_in_loop(klong_loop, run_in_klong(klong, content))


def start_loop(loop, stop_event):
    asyncio.set_event_loop(loop)
    loop.run_until_complete(stop_event.wait())


def setup_async_loop(start_loop_func, debug=False, slow_callback_duration=86400) -> asyncio.AbstractEventLoop:
    loop = asyncio.new_event_loop()
    loop.slow_callback_duration = slow_callback_duration
    if debug:
        loop.set_debug(True)
    stop_event = asyncio.Event()
    thread = threading.Thread(target=lambda l=loop,e=stop_event: start_loop_func(l,e), args=(loop,), daemon=True)
    thread.start()
    return loop, thread, stop_event


def cleanup_async_loop(loop: asyncio.AbstractEventLoop, loop_thread, stop_event, debug=True, name=None) -> None:

    if loop.is_closed():
        return

    loop.call_soon_threadsafe(stop_event.set)
    loop_thread.join()

    pending_tasks = asyncio.all_tasks(loop=loop)
    if len(pending_tasks) > 0:
        if name:
            print(f"WARNING: pending tasks in {name} loop")
        print(f"cancelling {len(pending_tasks)} pending tasks...")
        for task in pending_tasks:
            loop.call_soon_threadsafe(task.cancel)
        # wait for all tasks to be cancelled but we can't use run_until_complete because the loop is already running
        while len(asyncio.all_tasks(loop=loop)) > 0:
            time.sleep(0)

    loop.stop()

    if not loop.is_closed():
        loop.close()


def append_pkg_resource_path_KLONGPATH():
    with importlib.resources.as_file(importlib.resources.files('klongpy')) as pkg_path:
        pkg_lib_path = os.path.join(pkg_path, "lib")
        klongpath = os.environ.get("KLONGPATH", ".:lib")
        klongpath = f"{klongpath}:{pkg_lib_path}" if klongpath else str(pkg_lib_path)
        os.environ["KLONGPATH"] = klongpath


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 http://klongpy.org')
    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_true")
    parser.add_argument('filename', nargs='?', help='filename to be run if no flags are specified')

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

    if args.debug:
        print("args: ", args)

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

    klong = KlongInterpreter()

    io_loop, io_loop_thread, io_stop_event = setup_async_loop(start_loop, debug=args.debug)
    klong_loop, klong_loop_thread, klong_stop_event = setup_async_loop(start_loop, debug=args.debug)

    shutdown_event = CallbackEvent()

    append_pkg_resource_path_KLONGPATH()

    klong['.helpdb'] = []
    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:
        if args.verbose:
            print(f"Running: {args.filename}")
        run_file(klong_loop, klong, args.filename, verbose=args.verbose)

        def gather_io_tasks(io_loop):
            done_event = threading.Event()
            tasks = asyncio.all_tasks(loop=io_loop)
            async def main_task():
                gathered_task = asyncio.gather(*tasks)
                gathered_task.add_done_callback(lambda x: done_event.set())
                await gathered_task
            if len(tasks) > 1:
                io_loop.call_soon_threadsafe(asyncio.create_task, main_task())
            else:
                done_event.set()
            return done_event

        gather_io_tasks(io_loop).wait()
    else:
        run_repl = True

    if args.debug:
        print("run_repl: ", run_repl)

    if run_repl:
        console_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(console_loop)
        if args.load:
            if args.verbose:
                print(f"Loading: {args.load}")
            run_file(klong_loop, klong, args.load, verbose=args.verbose)
        colorama.init(autoreset=True)
        show_repl_header(args.server)
        console_loop.create_task(ConsoleInputHandler.input_producer(console_loop,   klong_loop, klong, args.verbose))
        console_loop.run_forever()
        console_loop.close()

    shutdown_event.trigger()

    cleanup_async_loop(io_loop, io_loop_thread, stop_event=io_stop_event, debug=args.debug, name="io_loop")
    cleanup_async_loop(klong_loop, klong_loop_thread, stop_event=klong_stop_event, debug=args.debug, name="klong_loop")
