#!/usr/bin/env python3

# This script generates a static documentation web site
# by parsing the `**/*.asiidoc` files from the repository.
#
# Dependencies:
# * Python 3
# * `xdg-open` for opening the final result in a web browser.
# * `antora` - a static documentation web site generator,
#     https://docs.antora.org/antora/latest
#
# Usage:
# ```console
# $ ./contrib/gendocs.py
# ```
#
# After running it should open the generated web site in a browser.
#


from dataclasses import dataclass
from typing import Optional
import glob
import itertools
import os
import pathlib
import re
import shutil
import subprocess

# Get the script directory.
script_dir = os.path.dirname(os.path.realpath(__file__))

# Switch to the projects root dir.
os.chdir(os.path.join(script_dir, ".."))

# Recreating the final output dir to start from scratch.
shutil.rmtree("doc_gen", ignore_errors=True)
os.makedirs("doc_gen", exist_ok=True)

# Antora fails if the repo contains broken symbolic links.
# shutil.rmtree("libexec", ignore_errors=True)

# Canonical Antora paths.
# See: https://docs.antora.org/antora/latest/standard-directories.
#      https://docs.antora.org/antora/latest/root-module-directory.
os.makedirs("doc_gen/modules/ROOT/images", exist_ok=True)
os.makedirs("doc_gen/modules/ROOT/pages", exist_ok=True)


# Put necessary images to the Antora canonical directory.
# See: https://docs.antora.org/antora/latest/images-directory.
for gif_file in glob.glob("doc/*.gif"):
    shutil.copy(gif_file, "doc_gen/modules/ROOT/images/")


# Fix links according to the Antora specification.
# See: https://docs.antora.org/antora/latest/page/xref.
def fix_links(path):
    @dataclass
    class Link:
        path: Optional[str]
        file: Optional[str]
        fragment: Optional[str]
        title: str

        def __init__(self, path, file, fragment, title):
            self.path = path
            self.file = file
            self.fragment = fragment
            self.title = title

    def dropwhile(predicate, string):
        return "".join(
            itertools.dropwhile(
                predicate,
                string,
            )
        )

    def dropwhileright(predicate, string):
        return "".join(
            reversed(
                list(
                    itertools.dropwhile(
                        predicate,
                        reversed(string),
                    )
                )
            )
        )

    def takewhile(predicate, string):
        return "".join(
            itertools.takewhile(
                predicate,
                string,
            )
        )

    def untag(string):
        no_opening = dropwhile(lambda c: c == "<", string)
        no_closing = dropwhileright(lambda c: c == ">", no_opening)
        return no_closing

    def parse(string):
        untagged = untag(string)
        prefix, title = untagged.split(",", 1)
        title = title.strip()
        fragment = dropwhile(lambda c: c != "#", prefix)
        fragment = fragment if fragment else None
        fragmentless = takewhile(lambda c: c != "#", prefix)
        segments = fragmentless.split("/")
        path, file = (
            ("/".join(segments[:-1]), segments[-1])
            if "/" in fragmentless
            else (None, fragmentless)
        )
        return Link(path, file, fragment, title)

    def render(link):
        if link.path and link.file and link.fragment == "#":
            return f"xref:{link.path}/{link.file}.adoc[{link.title}]"
        elif link.path and link.file and link.fragment:
            return f"xref:{link.path}/{link.file}.adoc{link.fragment}[{link.title}]"
        elif not link.path and link.file and link.fragment == "#":
            return f"xref:./{link.file}.adoc[{link.title}]"
        elif not link.path and link.file and link.fragment:
            return f"xref:./{link.file}.adoc{link.fragment}[{link.title}]"
        elif not link.path and link.file and not link.fragment:
            return f"<<{link.file},{link.title}>>"
        else:
            raise RuntimeError(f"Failed to render link: {link}")

    def process(m):
        string = m.group(0)
        return render(parse(string)) if "," in string else string

    content = None

    with open(path, "r") as file:
        content = file.read()

    # Fix image links according the Antora specification.
    # See: https://docs.antora.org/antora/latest/page/image-resource-id-examples.
    content = content.replace("image::doc/", "image::")

    with open(path, "w") as file:
        file.write(re.sub(r"<<[^>]+>>", process, content))


# A useful documentation page.
# Add the `.adoc` extension to include it into the result.
shutil.copy(
    "VIMTOKAK",
    "doc_gen/modules/ROOT/pages/VIMTOKAK.adoc",
)
fix_links("doc_gen/modules/ROOT/pages/VIMTOKAK.adoc")

for source in glob.glob("**/*.asciidoc", recursive=True):
    # Create directories structure matching the project's original structure.
    # See: https://docs.antora.org/antora/latest/pages-directory.
    page_dir = os.path.join(
        "doc_gen/modules/ROOT/pages",
        os.path.dirname(source),
    )
    os.makedirs(page_dir, exist_ok=True)

    # Copy the `asciidoc` file into the Antora `pages` directory
    # with the mandatory `.adoc` filename extension.
    adoc = os.path.join(page_dir, pathlib.Path(source).stem + ".adoc")
    shutil.copy(source, adoc)

    if source == "README.asciidoc":
        # Update the filename so it reflects the content.
        # The filename is used for navigation links.
        shutil.move(
            "doc_gen/modules/ROOT/pages/README.adoc",
            "doc_gen/modules/ROOT/pages/index.adoc",
        )
        adoc = "doc_gen/modules/ROOT/pages/index.adoc"
    elif source == "test/README.asciidoc":
        # The file name is used for navigation links.
        # Update so it reflects the content.
        shutil.move(
            "doc_gen/modules/ROOT/pages/test/README.adoc",
            "doc_gen/modules/ROOT/pages/test/tests.adoc",
        )
        adoc = "doc_gen/modules/ROOT/pages/test/tests.adoc"
    fix_links(adoc)


# A navigation file for the sidebar.
# See: https://docs.antora.org/antora/latest/navigation/single-list.
#
# TODO: Generate automatically.
nav_content = """
* xref:index.adoc[Getting Started]
* xref:doc/pages/commands.adoc[Commands]
* xref:doc/pages/expansions.adoc[Expansions]
* xref:doc/pages/execeval.adoc[Exec Eval]
* xref:doc/pages/scopes.adoc[Scopes]
* xref:doc/pages/faces.adoc[Faces]
* xref:doc/pages/buffers.adoc[Buffers]
* xref:doc/pages/registers.adoc[Registers]
* xref:doc/pages/mapping.adoc[Mapping]
* xref:doc/pages/hooks.adoc[Hooks]
* xref:doc/pages/command-parsing.adoc[Command Parsing]
* xref:doc/pages/keys.adoc[Keys]
* xref:doc/pages/regex.adoc[Regex]
* xref:doc/pages/options.adoc[Options]
* xref:doc/pages/highlighters.adoc[Highlighters]
* xref:doc/pages/modes.adoc[Modes]
* xref:doc/pages/keymap.adoc[KEYMAP]
* xref:doc/pages/faq.adoc[FAQ]
* xref:doc/pages/changelog.adoc[Changelog]
* xref:doc/design.adoc[Design]
* xref:doc/coding-style.adoc[Coding Style]
* xref:doc/writing_scripts.adoc[Writing Scripts]
* xref:doc/json_ui.adoc[JSON UI]
* xref:doc/autoedit.adoc[Autoedit]
* xref:doc/interfacing.adoc[Interfacing]
* xref:rc/tools/lint.adoc[Linting]
* xref:rc/tools/autorestore.adoc[Autorestore]
* xref:rc/tools/doc.adoc[Doc]
* xref:test/tests.adoc[Tests]
* xref:VIMTOKAK.adoc[Vi(m) to Kakoune]
"""

with open("doc_gen/modules/ROOT/nav.adoc", "w") as f:
    f.write(nav_content)

# Antora component description file.
# See: https://docs.antora.org/antora/latest/component-version-descriptor.
antora_yml_content = """
name: Kakoune
nav:
  - modules/ROOT/nav.adoc
title: Kakoune
version: latest
"""

with open("doc_gen/antora.yml", "w") as f:
    f.write(antora_yml_content)

# Antora playbook file.
# See: https://docs.antora.org/antora/latest/playbook.
antora_playbook_content = """
asciidoc:
  attributes:

    # Do not complain on missing attributes,
    # TODO: fix and turn to a fatal warning
    attribute-missing: skip

    # To fix links
    idprefix: ""

    # To fix links
    idseparator: "-"

    # Better to be reproducible, in general
    reproducible: true

    # More convenient to turn sections to IDs
    sectids: true

    # More convenient to have sections as links
    sectlinks: true

    # Do not want to miss something
    sectnumlevels: 5

    # More convenient to number the sections
    sectnums: true

    # Do not want to miss something
    toclevels: 5

  sourcemap: true

content:
  sources:
  - url: ./..
    start_path: doc_gen
    branches: ["HEAD"]

runtime:
  cache_dir: ./doc_gen/cache # More convenient for the development
  fetch: true # More convenient for the development

  log:
    failure_level: fatal
    level: warn

output:
  clean: true # More convenient for the development
  dir: ./build # Simpler to have it explicit in code

site:
  title: Kakoune Docs

ui:
  bundle:
    url: https://gitlab.com/antora/antora-ui-default/-/jobs/artifacts/HEAD/raw/build/ui-bundle.zip?job=bundle-stable
"""

with open("doc_gen/antora-playbook.yml", "w") as f:
    f.write(antora_playbook_content)

# Finally, generate the documentation,
# results will be saved to the output directory
# as specified in the `antora-playbook.yml`.
use_npx = shutil.which("antora") is None and shutil.which("npx") is not None
subprocess.run((["npx"] if use_npx else []) + ["antora", "generate", "doc_gen/antora-playbook.yml"])
subprocess.run(["xdg-open", "./doc_gen/build/Kakoune/latest/index.html"])
