"""Open pond.py's window for real, with nobody at the mouse, and print what happens.

    python pond_plays.py bot       a simple player swims for food and away from danger
    python pond_plays.py eaten     the fish waits in front of a hunter: the game ends,
                                   then the Space key starts a new game
    python pond_plays.py win       the fish starts bigger than every hunter and eats them
    python pond_plays.py hide      the fish sits under a leaf: every hunter wanders
    python pond_plays.py main      the file started through its own main(), for 6 seconds

Add the word "model" to play with the AI model, when the folder "model" is beside
pond.py:    python pond_plays.py bot model

One scenario for each run: tkinter wants one window system in a process. The mouse is
moved with Tk's own events (event_generate), so keep the real mouse out of the window.
Add a folder name to save pictures of the window there (this needs Pillow, on Windows):
    python pond_plays.py bot rules pictures
"""
import json
import math
import pathlib
import runpy
import sys
import time
import tkinter as tk

ASSETS = pathlib.Path(__file__).resolve().parent.parent      # pond.py: one folder up from checks/


def picture(game, folder, name):
    """Save a picture of the game's canvas. Does nothing when no folder was given."""
    if not folder:
        return
    from PIL import ImageGrab
    game.root.update()
    canvas = game.canvas
    left, top = canvas.winfo_rootx(), canvas.winfo_rooty()
    box = (left, top, left + canvas.winfo_width(), top + canvas.winfo_height())
    pathlib.Path(folder).mkdir(parents=True, exist_ok=True)
    ImageGrab.grab(box).save(str(pathlib.Path(folder) / (name + ".png")))


def build(pond, brain):
    game = pond.Game(brain)
    game.root.geometry("+60+40")
    game.root.attributes("-topmost", True)       # nothing may cover it while a picture is taken
    # Count the frames the game itself draws: every frame() calls draw() once.
    game.drawn = {"frames": 0, "last": time.time(), "longest_wait": 0.0, "longest_at": 0.0,
                  "started": time.time()}
    real_draw = game.draw

    def counting_draw():
        now = time.time()
        drawn = game.drawn
        drawn["frames"] += 1
        if now - drawn["last"] > drawn["longest_wait"]:
            drawn["longest_wait"] = now - drawn["last"]
            drawn["longest_at"] = now - drawn["started"]
        drawn["last"] = now
        real_draw()

    game.draw = counting_draw
    return game


def frames_report(game):
    """How smoothly the window drew: frames a second, and the longest stop."""
    drawn = game.drawn
    took = time.time() - drawn["started"]
    return {"frames": drawn["frames"], "seconds": round(took, 1),
            "frames_per_second": round(drawn["frames"] / took, 1),
            "longest_wait_between_frames": round(drawn["longest_wait"], 2),
            "that_wait_was_at_second": round(drawn["longest_at"], 1)}


def steer(pond, game):
    """Where a simple player wants to be: at the nearest food, or away from danger."""
    head = game.player.points[0]
    danger = None
    for hunter in game.hunters:
        if hunter.size <= game.player.size:
            continue
        far = math.dist(head, hunter.points[0])
        if far < 170 and (danger is None or far < danger[0]):
            danger = (far, hunter)
    if danger:
        # Swim straight away from the nearest dangerous head, and stay in the pond.
        away = pond.walk(head, pond.angle_to(danger[1].points[0], head), 160)
        return [min(max(away[0], 30), pond.WIDE - 30), min(max(away[1], 30), pond.HIGH - 30)]
    prey = [hunter for hunter in game.hunters if hunter.size < game.player.size]
    if prey:
        nearest = min(prey, key=lambda hunter: math.dist(head, hunter.points[0]))
        return nearest.points[len(nearest.points) // 2]
    return min(game.food, key=lambda dot: math.dist(head, dot))


def scenario_bot(pond, brain, folder, seconds=75):
    game = build(pond, brain)
    started = time.time()
    log = {"events": []}
    seen = {"size": game.player.size, "hunters": len(game.hunters), "doing": {}, "shots": 0}

    def note(text):
        log["events"].append(f"{time.time() - started:6.2f}s  {text}")

    def finish(why):
        picture(game, folder, "bot_end")
        print(json.dumps({"brain_at_start": brain, "brain_at_end": game.brain, "note": game.note,
                          "why_it_stopped": why, "events": log["events"],
                          "size": game.player.size, "hunters_left": len(game.hunters),
                          "window": frames_report(game)}, indent=1))
        game.root.destroy()

    def watch():
        now = time.time() - started
        goal = steer(pond, game)
        game.canvas.event_generate("<Motion>", x=int(goal[0]), y=int(goal[1]))
        if game.player.size != seen["size"]:
            seen["size"] = game.player.size
        if len(game.hunters) != seen["hunters"]:
            note(f"a hunter was eaten: {len(game.hunters)} left, your size {game.player.size}")
            seen["hunters"] = len(game.hunters)
        for number, hunter in enumerate(game.hunters):
            key = id(hunter)
            if seen["doing"].get(key) != hunter.doing:
                note(f"{hunter.kind} {hunter.size} -> {hunter.doing:6}  ({pond.in_words(*game.facts(hunter))})")
                seen["doing"][key] = hunter.doing
        if now > 4 and seen["shots"] == 0:
            picture(game, folder, "bot_4s")
            seen["shots"] = 1
        if now > 20 and seen["shots"] == 1:
            picture(game, folder, "bot_20s")
            seen["shots"] = 2
        if game.over:
            note("OVER: " + game.over)
            finish("the game ended")
            return
        if now > seconds:
            finish("time was up")
            return
        game.root.after(16, watch)

    game.frame()
    game.root.after(16, watch)
    game.root.mainloop()


def scenario_eaten(pond, brain, folder):
    game = build(pond, brain)
    result = {}
    hunter = game.hunters[0]
    # Stand still, 120 pixels in front of the first hunter's nose.
    place = pond.walk(hunter.points[0], hunter.angles[0], 120)
    game.canvas.event_generate("<Motion>", x=int(place[0]), y=int(place[1]))
    started = time.time()

    def watch():
        if game.over and "over" not in result:
            result["over"] = game.over
            result["seconds_until_eaten"] = round(time.time() - started, 1)
            picture(game, folder, "eaten")
            game.root.event_generate("<space>")
            game.root.after(300, after_space)
            return
        if time.time() - started > 60:
            result["over"] = "NOT EATEN IN 60 s"
            print(json.dumps(result, indent=1))
            game.root.destroy()
            return
        game.root.after(30, watch)

    def after_space():
        result["after_space"] = {"over": game.over, "size": game.player.size,
                                 "hunters": len(game.hunters), "food": len(game.food)}
        print(json.dumps(result, indent=1))
        game.root.destroy()

    game.frame()
    game.root.after(30, watch)
    game.root.mainloop()


def scenario_win(pond, brain, folder):
    game = build(pond, brain)
    game.player.resize(150)                      # bigger than every hunter
    started = time.time()
    result = {"sizes": []}

    def watch():
        if game.hunters:
            head = game.player.points[0]
            nearest = min(game.hunters, key=lambda hunter: math.dist(head, hunter.points[0]))
            goal = nearest.points[len(nearest.points) // 2]
            game.canvas.event_generate("<Motion>", x=int(goal[0]), y=int(goal[1]))
            if not result["sizes"] or result["sizes"][-1] != game.player.size:
                result["sizes"].append(game.player.size)
        if game.over:
            result["over"] = game.over
            result["seconds"] = round(time.time() - started, 1)
            result["doing_was"] = sorted(result.pop("doing", []))
            picture(game, folder, "win")
            print(json.dumps(result, indent=1))
            game.root.destroy()
            return
        result.setdefault("doing", set()).update(hunter.doing for hunter in game.hunters)
        if time.time() - started > 120:
            result["over"] = "NO WIN IN 120 s"
            result["doing_was"] = sorted(result.pop("doing", []))
            print(json.dumps(result, indent=1))
            game.root.destroy()
            return
        game.root.after(16, watch)

    game.frame()
    game.root.after(16, watch)
    game.root.mainloop()


def scenario_hide(pond, brain, folder):
    game = build(pond, brain)
    leaf = pond.LEAVES[0]
    game.canvas.event_generate("<Motion>", x=leaf[0], y=leaf[1])
    started = time.time()
    seconds = 25 if brain == "model" else 8

    def finish():
        head = game.player.points[0]
        picture(game, folder, "hide")
        print(json.dumps({"brain_at_start": brain, "brain_at_end": game.brain, "note": game.note,
                          "to_leaf_centre": round(math.dist(head, leaf)),
                          "hidden": game.under_leaf(),
                          "doing": [hunter.kind + ": " + hunter.doing for hunter in game.hunters],
                          "over": game.over, "window": frames_report(game)}, indent=1))
        game.root.destroy()

    def hold():
        # Say it again every frame: a real mouse that crosses the window moves the fish too.
        game.canvas.event_generate("<Motion>", x=leaf[0], y=leaf[1])
        if time.time() - started > seconds:
            finish()
            return
        game.root.after(16, hold)

    game.frame()
    game.root.after(16, hold)
    game.root.mainloop()


def scenario_main(pond, brain, folder):
    real_mainloop = tk.Tk.mainloop

    def short_mainloop(self, n=0):
        self.after(6000, self.destroy)          # close the window by itself after 6 seconds
        real_mainloop(self, n)

    tk.Tk.mainloop = short_mainloop
    runpy.run_path(str(ASSETS / "pond.py"), run_name="__main__")
    print(json.dumps({"main": "ran for 6 s and closed"}))


def main():
    scenarios = {"bot": scenario_bot, "eaten": scenario_eaten, "win": scenario_win,
                 "hide": scenario_hide, "main": scenario_main}
    words = sys.argv[1:]
    if not words or words[0] not in scenarios:
        print(__doc__)
        return 2
    brain = "rules"
    if len(words) > 1:
        brain = words[1]
    folder = words[2] if len(words) > 2 else ""
    if sys.platform == "win32":
        # Without this, a picture of the window is a magnified corner of it at 125% scaling.
        import ctypes
        ctypes.windll.shcore.SetProcessDpiAwareness(1)
    sys.path.insert(0, str(ASSETS))
    import pond
    scenarios[words[0]](pond, brain, folder)
    return 0


if __name__ == "__main__":
    sys.exit(main())
