#!/usr/bin/env python3
# Generate the man page from the Alire builtin help
#
# It assumes the alr command is available in 'bin/alr'
# Man pages are produced in doc/man1.  Run with:
#
#  python3 scripts/python/generate-man.py
#
# To check a man page, setup:
#
#  export MANPATH=`pwd`/doc
#
# and then type `man alr` or `man alr-with`

from collections.abc import Generator, Iterable
import os
import re
import subprocess
import enum
import sys


EXECUTABLE = "bin/alr"
DESTINATION_DIR = "debian/man1"
GENERAL_COMMANDS = ("config", "printenv", "toolchain", "version")
INDEX_COMMANDS = ("get", "index", "init", "pin", "search", "show",
                  "update", "with")
BUILD_COMMANDS = ("action", "build", "clean", "dev", "edit", "run",
                  "test", "exec")
PUBLISH_COMMANDS = ("publish", )
DATE = "Aug 3, 2022"
VERSION = "1.2"


def alr_help(section: str) -> Generator[str, None, None]:
    """
    The output of 'alr help {section}', line by line,
    without trailing or leading blanks.
    """
    proc = subprocess.run((EXECUTABLE, 'help', section),
                          capture_output=True,
                          encoding='utf-8',
                          check=False)
    if proc.returncode or proc.stderr:
        print('Command failed', proc)
        sys.exit(1)

    for line in proc.stdout.splitlines():
        yield line.strip()


class State(enum.Enum):
    In_Unkown = 0
    In_Summary = 1
    In_Synopsis = 2
    In_Options = 3
    In_Description = 4
    In_Other = 5


class ManGenerator:

    def __init__(self, name: str) -> None:
        self.state = State.In_Unkown
        self.name = name
        self.summary = ''
        self.previous_line_empty = False
        self.preformat_mode = False

    def path(self) -> str:
        """Expected name of the file where to write this manual page."""
        return os.path.join(DESTINATION_DIR, self.name + '.1')

    def emit_header(self) -> Generator[str, None, None]:
        title_upper = self.name.upper()
        yield f".TH {title_upper} 1 \"{DATE}\" \"Alire {VERSION}\" \"Alire manual\"\n"
        yield ".nh\n"
        yield ".ad l\n"
        yield ".SH NAME\n"
        yield f"{self.name} \\- {self.summary}\n"
        yield ".\\\"\n"
        yield ".SH SYNOPSIS\n"
        yield ".sp\n"

    def emit_footer(self, commands: Iterable[str]) -> Generator[str, None, None]:
        yield "\n.SH SEE ALSO\n"
        if self.name != "alr":
            yield "\\fIalr(1)\\fR, "
        for command in commands:
            if self.name != f"alr-{command}":
                yield f"\\fIalr-{command}(1)\\fR, "
        yield "\\fIgprbuild(1)\\fR"
        yield "\n.SH AUTHOR\n"
        yield "Generated with generate-man from Alire execution\n"

    def emit_option(self, line: str) -> Generator[str, None, None]:
        items = line.split()
        if len(items) == 0:
            return

        yield ".TP 5\n"
        first = True
        check_description = True
        for item in items:
            item = re.sub(r'=([^\)]*)', r'=\\fI\1\\fP', item)
            if first:
                item = re.sub(r'(\[.*\])', r'\\fI\1\\fP', item)
                yield item
                first = False
            elif check_description and item[0] == '(':
                item = re.sub(r'[\(\)]', r'', item)
                yield ', '
                yield item
            elif check_description and item.istitle():
                check_description = False
                yield "\n"
                yield item
            else:
                yield ' '
                yield item
        yield "\n"

    def emit_synopsis(self, line: str) -> Generator[str, None, None]:
        line = re.sub(r'\-', r'\\-', line)
        items = line.split()
        if len(items) == 0:
            return
        is_cmd = True
        need_separator = False
        yield "\\fI"
        for item in items:
            if is_cmd and item[0] == '[':
                yield "\\fP"
                is_cmd = False
            if need_separator:
                yield " "
            yield item
            need_separator = True
        yield "\n"

    def emit_description(self, line: str) -> Generator[str, None, None]:
        if line == "":
            if not self.previous_line_empty:
                yield ".PP\n"
            self.previous_line_empty = True
        else:
            if line.startswith("'"):
                # Add a zero-width space character in front of
                # lines looking like an unexpected groff macro.
                yield "\\&"
            # Replace "-P xxx" by \fI-P xxx\fP
            line = re.sub(r'\"(\-[^\"]*)\"', r'\\fI\1\\fP', line)
            line = re.sub(r'--([a-zA-Z_]*)', r'\\fI--\1\\fP', line)
            line = re.sub(r'`([a-zA-Z]*)`', r'\\fI\1\\fR', line)
            if re.match(r'^\* .*:', line):
                line = re.sub(r'^\* ', r'', line)
                line = re.sub(r':$', r'', line)
                yield f".SS {line}\n"

            elif re.match(r'^-[ ]+[a-z]*.*\[.*\]', line):
                line = re.sub(r'^-[ ]*', r'', line)
                yield f".SS {line}\n"

            elif (re.match(r'^[A-Z]: ', line)
                  or re.match(r'^crate.*[ \t][A-Z].*', line)):
                if self.preformat_mode:
                    yield ".br\n"
                else:
                    yield ".PP\n"
                self.preformat_mode = True
                yield f"{line}\n"

            elif self.preformat_mode:
                self.preformat_mode = False
                yield ".PP\n"
                yield f"{line}\n"

            else:
                yield f"{line}\n"

    def emit(self, line: str) -> Generator[str, None, None]:
        if line == "SUMMARY":
            self.state = State.In_Summary

        elif line == "USAGE":
            yield from self.emit_header()
            self.state = State.In_Synopsis

        elif line in ("ARGUMENTS", "OPTIONS"):
            yield ".\\\"\n"
            yield ".SH OPTIONS\n"
            self.state = State.In_Options

        elif line == "GLOBAL OPTIONS":
            yield ".\\\"\n"
            yield ".SH GLOBAL OPTIONS\n"
            self.state = State.In_Options

        elif line == "DESCRIPTION":
            yield ".\\\"\n"
            yield ".SH DESCRIPTION\n"
            self.state = State.In_Description

        elif line in ("TOPICS", "ALIASES", "COMMANDS"):
            yield ".\\\"\n"
            yield f".SH {line}\n"
            self.state = State.In_Other

        elif self.state == State.In_Summary:
            self.summary = self.summary + line

        elif self.state == State.In_Description:
            yield from self.emit_description(line)

        elif self.state == State.In_Options:
            yield from self.emit_option(line)

        elif self.state == State.In_Synopsis:
            yield from self.emit_synopsis(line)

        else:
            yield f"{line}\n"

    def generate(self, name: str, commands: Iterable[str]) -> None:
        """
        Generate an alire man page for a specific command
        The 'bin/alr help {name}' command is executed and parsed to re-format following the man format.
        """
        with open(self.path(), "w", encoding='utf-8') as outfile:
            for line in alr_help(name):
                for chunk in self.emit(line):
                    outfile.write(chunk)

            for chunk in self.emit_footer(commands):
                outfile.write(chunk)

    def generate_main(self) -> None:
        """
        Generate the alire main man page
        """
        with open(self.path(), "w", encoding='utf-8') as outfile:
            self.summary = "Ada Library Repository"
            for chunk in self.emit_header():
                outfile.write(chunk)
            outfile.write("\\fIalr\\fP [global options] <command> [command options]] [<arguments>]\n")
            outfile.write(".SH DESCRIPTION\n")
            with open("doc/introduction.md", "r", encoding='utf-8') as intro:
                # Skip first line
                intro.readline()
                for line in intro:
                    line = re.sub(r'`([a-zA-Z]*)`', r'\\fI\1\\fR', line)
                    outfile.write(line)

            # run alr help aliases to print the aliases section

            for section in ("aliases", "identifiers", "toolchains"):
                lines = alr_help(section)
                outfile.write(f".SS {next(lines)}\n")
                for line in lines:
                    if line.startswith("'"):
                        # Add a zero-width space character in front of
                        # lines looking like an unexpected groff macro.
                        outfile.write("\\&")
                    for chunk in self.emit(line):
                        outfile.write(chunk)

            for chunk in self.emit_footer(INDEX_COMMANDS):
                outfile.write(chunk)


def main() -> None:
    if not os.path.exists(DESTINATION_DIR):
        os.mkdir(DESTINATION_DIR)

    generator = ManGenerator("alr")
    generator.generate_main()

    for commands in (GENERAL_COMMANDS, INDEX_COMMANDS, BUILD_COMMANDS,
                     PUBLISH_COMMANDS):
        for name in commands:
            generator = ManGenerator('alr-' + name)
            generator.generate(name, commands)


main()
