#!/usr/bin/env python3
"""
Choose a man page to read interactively from a menu
"""

import argparse
import asyncio
import os
import re
from typing import Any, Generator, List, Optional, Sequence, cast

import ptvertmenu
from prompt_toolkit import Application
from prompt_toolkit.application import get_app
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.key_binding.bindings.focus import focus_next
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
from prompt_toolkit.layout.containers import VSplit
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
from ptvertmenu.vertmenu import Item

E = KeyPressEvent

PATH = "/usr/share/man"

ManItem = tuple[Any, tuple[int, str]]


def generator(section: Optional[int] = None) -> Generator[ManItem, None, None]:
    labelre = re.compile(
        re.escape(PATH)
        + r"/(?P<label>man(?P<section>[0-9]+)/(?P<base>.*))\.[0-9]\S*\.gz"
    )
    for root, _, files in os.walk(PATH):
        for filename in files:
            path = os.path.join(root, filename)
            m = labelre.match(path)
            if not m:
                continue
            if section is not None and int(m.group("section")) != section:
                continue
            yield (m.group("label"), (int(m.group("section")), m.group("base")))


async def man_loader(
    contents: TextArea, queue: asyncio.Queue[Optional[tuple[int, str]]]
) -> None:
    while True:
        item = None
        item = await queue.get()
        while not queue.empty():
            item = await queue.get()
        if item is None:
            contents.text = ""
            queue.task_done()
            continue
        contents.text = f"Loading {item[1]}..."
        manwidth = ""
        if contents.window.render_info:
            manwidth = f"MANWIDTH={contents.window.render_info.window_width - 1} "
        man = await asyncio.create_subprocess_shell(
            f"{manwidth}man --encoding=utf-8 {str(item[0])} {item[1]}",
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.STDOUT,
        )
        (manpage, _) = await man.communicate()
        manpage = re.sub(rb"\x1b\[[0-9;]*m?", b"", manpage)
        contents.text = manpage.decode("utf-8", errors="ignore").replace("\t", "    ")
        queue.task_done()


async def manmenu(
    section: Optional[int] = None, menu_max_width: Optional[int] = None
) -> None:
    items: List[ManItem] = list(generator(section=section))
    items.sort()
    contents = TextArea(text="", multiline=True, wrap_lines=True, read_only=True)
    manloader_queue: asyncio.Queue[Optional[tuple[int, str]]] = asyncio.Queue()
    manloader_task = asyncio.create_task(man_loader(contents, manloader_queue))

    def selected_handler(item: Optional[ManItem], index: Optional[int]) -> None:
        if item is not None:
            manloader_queue.put_nowait(item[1])
        else:
            manloader_queue.put_nowait(None)

    def accept_handler(item: ManItem) -> None:
        get_app().layout.focus(contents)

    menu = ptvertmenu.FuzzFilterVertMenu(
        items=cast(Sequence[Item], items),
        selected_handler=selected_handler,
        accept_handler=accept_handler,
        menu_max_width=menu_max_width,
    )
    root_container = VSplit(
        [
            Frame(title="Man pages", body=menu),
            Frame(title="Contents", body=contents),
        ]
    )
    layout = Layout(root_container)
    layout.focus(menu)
    # Use a basic style
    style = Style.from_dict(
        {
            "vertmenu.focused vertmenu.selected": "bold fg:white bg:red",
            "vertmenu.unfocused vertmenu.selected": "fg:white bg:darkred",
            "vertmenu.unfocused vertmenu.item": "fg:grey bg:black",
        }
    )
    kb = KeyBindings()
    app: Application[None] = Application(
        layout=layout,
        key_bindings=kb,
        full_screen=True,
        style=style,
        mouse_support=True,
    )

    @kb.add("tab")
    def tab(event: E) -> None:
        focus_next(event)

    @kb.add("c-c")
    @kb.add("c-d")
    @kb.add("escape", "q")
    def close(event: E) -> None:
        manloader_task.cancel()
        app.exit()

    await app.run_async()


async def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "--section",
        type=int,
        default=None,
        help="Show only the specified man section",
    )
    parser.add_argument(
        "--menu-max-width",
        type=int,
        default=None,
        help="Max width of the menu on the left",
    )
    parser.add_argument(
        "--version", "-V", action="version", version="%(prog)s " + ptvertmenu.version()
    )
    args = parser.parse_args()
    await manmenu(section=args.section, menu_max_width=args.menu_max_width)


if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())
