#!/usr/bin/env python3
"""
Preprocesses mdbook markdown, primarily for include directives.

The include directive format is as follows:
    {{#include foo/bar/baz.md}}

The content of includes will be indented as much as the directive itself.

Including a generated file (from building Lix; generally for the 'new' CLI):
    {{#include @generated@/foo/bar/baz.md}}
"""

from pathlib import Path
import json
import os, os.path
import sys
import textwrap

name = 'substitute.py'

def log(*args, **kwargs):
    kwargs['file'] = sys.stderr
    return print(f'{name}:', *args, **kwargs)

def remove_prefix_if_present(s: str, prefix: str) -> str | None:
    if s.startswith(prefix):
        return s.removeprefix(prefix)
    else:
        return None

def do_include(content: str, relative_md_path: Path, source_root: Path, search_path: Path):
    assert not relative_md_path.is_absolute(), f'{relative_md_path=} from mdbook should be relative'

    md_path_abs = source_root / relative_md_path
    var_abs = md_path_abs.parent
    assert var_abs.is_dir(), f'supposed directory {var_abs} is not a directory (cwd={os.getcwd()})'

    lines = []
    for l in content.splitlines(keepends=True):
        if remain := remove_prefix_if_present(l.strip(), "{{#include"):
            requested = remain[1:-2]
            # We indent bodies of indent directives by the indent of the
            # directive itself.
            num_leading_indent = len(l) - len(l.lstrip())

            if subpath := remove_prefix_if_present(requested, "@generated@/"):
                included = search_path / Path(subpath)
                requested = included.relative_to(search_path)
            else:
                included = source_root / relative_md_path.parent / requested
                requested = included.resolve().relative_to(source_root)
            assert included.exists(), f"{requested} not found at {included}"

            lines.append(
                textwrap.indent(
                    do_include(
                        included.read_text(),
                        requested,
                        source_root,
                        search_path,
                    ),
                    " " * num_leading_indent
                )
                + "\n"
            )
        else:
            lines.append(l)
    return "".join(lines)

def recursive_replace(data, book_root, search_path):
    match data:
        # XXX FUTURE: drop sections once mdBook is at 0.5.0 or above in nixpkgs
        case {'sections': sections}:
            return data | dict(
                sections = [recursive_replace(section, book_root, search_path) for section in sections],
            )
        case {'items': items}:
            return data | dict(
                items = [recursive_replace(item, book_root, search_path) for item in items],
            )
        case {'Chapter': chapter}:
            path_to_chapter = Path(chapter['path'])
            chapter_content = chapter['content']

            return data | dict(
                Chapter = chapter | dict(
                    # first process includes. this must happen before docroot processing since
                    # mdbook does not see these included files, only the final agglomeration.
                    content = do_include(
                        chapter_content,
                        path_to_chapter,
                        book_root,
                        search_path
                    ).replace(
                        '@docroot@',
                        ("../" * len(path_to_chapter.parent.parts) or "./")[:-1]
                    ).replace(
                        # this replacement is to avoid corrupting the
                        # hacking.md manual section on docroot
                        '@\\docroot\\@',
                        '@docroot@',
                    ),
                    sub_items = [
                        recursive_replace(sub_item, book_root, search_path)
                        for sub_item in chapter['sub_items']
                    ],
                ),
            )

        case rest:
            assert False, f'should have been called on a dict, not {type(rest)=}\n\t{rest=}'

def main():

    if len(sys.argv) > 1 and sys.argv[1] == 'supports':
        return 0

    # mdbook communicates with us over stdin and stdout.
    # It splorks us a JSON array, the first element describing the context,
    # the second element describing the book itself,
    # and then expects us to send it the modified book JSON over stdout.

    context, book = json.load(sys.stdin)

    # book_root is the directory where book contents leave (ie, src/)
    book_root = Path(context['root']) / context['config']['book'].get('src', 'src')

    # includes pointing into @generated@ will look here
    search_path = Path(os.environ['MANUAL_SUBSTITUTE_SEARCH'])

    # Find @var@ in all parts of our recursive book structure.
    replaced_content = recursive_replace(book, book_root, search_path)

    replaced_content_str = json.dumps(replaced_content)

    # Give mdbook our changes.
    print(replaced_content_str)

try:
    sys.exit(main())
except AssertionError as e:
    print(f'{name}: INTERNAL ERROR in mdbook preprocessor', file=sys.stderr)
    print(f'this is a bug in {name}', file=sys.stderr)
    raise
