#!/usr/bin/env python
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Contributors to the OpenTimelineIO project

"""Generates documentation of all the built in plugins for OpenTimelineIO"""

# @TODO: HookScripts and Hooks don't have formatters.  Because OTIO doesn't
#        ship with any, I've left places for them but haven't fleshed them out.

import argparse
import tempfile
import textwrap

import io

import opentimelineio as otio


# force using the same path separator regardless of what the OS says
# this ensures same behavior on windows and linux
PATH_SEP = "/"

ALL_PLUGINS_TEXT = """This documents all the plugins that otio could find."""

PUBLIC_ONLY_TEXT = """This documents all the plugins that ship with in the open
source OpenTimelineIO distribution.
"""

DOCUMENT_HEADER = """# Plugin Documentation

{plugin_header_text}

This document is automatically generated by running the
`autogen_plugin_documentation` command, or by running `make plugin-model`.  It
is part of the unit tests suite and should be updated whenever the schema
changes.  If it needs to be updated, run: `make doc-plugins-update` and this
file should be regenerated.

# Manifests

The manifests describe plugins that are visible to OpenTimelineIO.  The core
manifest is listed first, then any user-defined local plugins.

{manifests}

# Core Plugins

Manifest path: `{manifest_path}`

{manifest_contents}

{local_manifest_text}
"""

MANIFEST_CONTENT_TEMPLATE = """

## Adapter Plugins

Adapter plugins convert to and from OpenTimelineIO.

[Adapters documentation page for more information](./adapters).

[Tutorial on how to write an adapter](write-an-adapter).

{adapters}

## Media Linkers

Media Linkers run after the adapter has read in the file and convert the media
references into valid references where appropriate.

[Tutorial on how to write a Media Linker](write-a-media-linker).

{media_linkers}

## SchemaDefs

SchemaDef plugins define new external schema.

[Tutorial on how to write a schemadef](write-a-schemadef).

{schemadefs}

## HookScripts

HookScripts are extra plugins that run on _hooks_.

[Tutorial on how to write a hookscript](write-a-hookscript).

{hook_scripts}

## Hooks

Hooks are the points at which hookscripts will run.

{hooks}

"""

LOCAL_MANIFEST_TEMPLATE = """
# Local Manifests

Local manifests found:
{manifest_paths}

{local_manifest_body}
"""

PLUGIN_TEMPLATE = """
### {name}

```
{doc}
```

*source*: `{path}`

{other}

"""

ADAPTER_TEMPLATE = """
*Supported Features (with arguments)*:

{}

"""

SCHEMADEF_TEMPLATE = """
*Serializable Classes*:

{}
"""


def _parsed_args():
    """ parse commandline arguments with argparse """

    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "-d",
        "--dryrun",
        action="store_true",
        default=False,
        help="Dryrun mode - print out instead of perform actions"
    )
    group.add_argument(
        "-o",
        "--output",
        type=str,
        default=None,
        help="Update the baseline with the current version"
    )
    parser.add_argument(
        "-p",
        "--public-only",
        default=False,
        action="store_true",
        help=(
            "Only include plugins defined in the public open source manifests."
            "  Used by unit test."
        )
    )
    parser.add_argument(
        "-s",
        "--sanitized-paths",
        default=False,
        action="store_true",
        help="Sanitize paths to only show last three directories in a path."
    )

    return parser.parse_args()


def _format_plugin(plugin_map, extra_stuff, sanitized_paths):
    # XXX: always force unix path separator so that the output is consistent
    # between every platform.
    PATH_SEP = "/"

    path = plugin_map['path']

    # force using PATH_SEP in place of os.path.sep
    path = path.replace("\\", PATH_SEP)

    if sanitized_paths:
        path = PATH_SEP.join(path.split(PATH_SEP)[-3:])
    return PLUGIN_TEMPLATE.format(
        name=plugin_map['name'],
        doc=plugin_map['doc'],
        path=path,
        other=extra_stuff,
    )


def _format_doc(docstring, prefix):
    """Use textwrap to format a docstring for markdown."""

    initial_indent = prefix
    # subsequent_indent = " " * len(prefix)
    subsequent_indent = " " * 2

    block = docstring.split("\n")
    fmt_block = []
    for line in block:
        line = textwrap.fill(
            line,
            initial_indent=initial_indent,
            subsequent_indent=subsequent_indent,
            width=len(subsequent_indent) + 80,
        )
        initial_indent = subsequent_indent
        fmt_block.append(line)

    return "\n".join(fmt_block)


def _format_adapters(plugin_map):
    feature_lines = []

    for feature, feature_data in plugin_map['supported features'].items():
        doc = feature_data['doc']
        if doc:
            feature_lines.append(
                _format_doc(doc, f"- {feature}: \n```\n") + "\n```"
            )
        else:
            feature_lines.append(
                f"- {feature}:"
            )

        for arg in feature_data["args"]:
            feature_lines.append(f"  - {arg}")

    return ADAPTER_TEMPLATE.format("\n".join(feature_lines))


def _format_schemadefs(plugin_map):
    feature_lines = []

    for sd in plugin_map['SchemaDefs'].keys():
        doc = plugin_map['SchemaDefs'][sd]['doc']
        if doc:
            feature_lines.append(
                _format_doc(doc, f"- {sd}: \n```\n") + "\n```"
            )
        else:
            feature_lines.append(f"- {sd}:")

    return SCHEMADEF_TEMPLATE.format("\n".join(feature_lines))


_PLUGIN_FORMAT_MAP = {
    "adapters": _format_adapters,
    "schemadefs": _format_schemadefs,
}


def _manifest_formatted(
        plugin_info_map,
        manifest_paths=None,
        sanitized_paths=False
):
    display_map = {}

    for pt in otio.plugins.manifest.OTIO_PLUGIN_TYPES:
        pt_lines = []

        if pt == "hooks":
            # hooks get handled differently by plugin_info_map() so we will skip them
            display_map[pt] = ""
            continue

        sorted_plugins = [
            plugin_info_map[pt][name]
            for name in sorted(plugin_info_map[pt].keys())
        ]

        for plug in sorted_plugins:
            if "ERROR" in plug or not plug:
                continue

            # filter out plugins from other manifests
            if manifest_paths and plug['from manifest'] not in manifest_paths:
                continue

            plugin_stuff = ""
            try:
                plugin_stuff = _PLUGIN_FORMAT_MAP[pt](plug)
            except KeyError:
                pass

            plug_lines = _format_plugin(plug, plugin_stuff, sanitized_paths)

            pt_lines.append(plug_lines)

        display_map[pt] = "\n".join(str(line) for line in pt_lines)

    return MANIFEST_CONTENT_TEMPLATE.format(
        adapters=display_map['adapters'],
        media_linkers=display_map['media_linkers'],
        schemadefs=display_map['schemadefs'],
        hook_scripts=display_map['hook_scripts'],
        hooks=display_map['hooks'],
    )


def generate_and_write_documentation_plugins(
        public_only=False,
        sanitized_paths=False
):
    md_out = io.StringIO()

    plugin_info_map = otio.plugins.plugin_info_map()

    # start with the manifest list
    manifest_path_list = plugin_info_map['manifests'][:]

    if public_only:
        # keep only core manifests
        manifest_path_list = [
            p for p in manifest_path_list
            if p.replace("\\", PATH_SEP).split(PATH_SEP)[-3] == "opentimelineio"
        ]

    sanitized_path_list = manifest_path_list.copy()

    if sanitized_paths:
        # conform all paths to unix-style path separators and leave relative
        # paths (relative to root of OTIO directory)
        sanitized_path_list = [
            PATH_SEP.join(p.replace("\\", PATH_SEP).split(PATH_SEP)[-3:])
            for p in sanitized_path_list
        ]

    manifest_list = "\n".join(f"- `{mp}`" for mp in sanitized_path_list)

    core_manifest_path = manifest_path_list[0]
    core_manifest_path_sanitized = sanitized_path_list[0]
    core_manifest_text = _manifest_formatted(
        plugin_info_map,
        [core_manifest_path],
        sanitized_paths
    )

    local_manifest_text = ""
    if len(plugin_info_map) > 2 and not public_only:
        local_manifest_paths = manifest_path_list[2:]
        local_manifest_paths_sanitized = sanitized_path_list[2:]
        local_manifest_list = "\n".join(
            f"- `{mp}`" for mp in local_manifest_paths_sanitized
        )
        local_manifest_body = _manifest_formatted(
            plugin_info_map,
            local_manifest_paths,
            sanitized_paths
        )
        local_manifest_text = LOCAL_MANIFEST_TEMPLATE.format(
            manifest_paths=local_manifest_list,
            local_manifest_body=local_manifest_body,
        )

    header = PUBLIC_ONLY_TEXT if public_only else ALL_PLUGINS_TEXT

    md_out.write(
        DOCUMENT_HEADER.format(
            plugin_header_text=header,
            manifests=manifest_list,
            manifest_path=core_manifest_path_sanitized,
            manifest_contents=core_manifest_text,
            local_manifest_text=local_manifest_text,
        )
    )

    return md_out.getvalue()


def main():
    """  main entry point  """

    args = _parsed_args()

    docs = generate_and_write_documentation_plugins(
        args.public_only,
        args.sanitized_paths
    )

    # print it out somewhere
    if args.dryrun:
        print(docs)
        return

    output = args.output
    if not output:
        output = tempfile.NamedTemporaryFile(
            'w',
            suffix="otio_serialized_schema.md",
            delete=False
        ).name

    with open(output, 'w') as fo:
        fo.write(docs)

    print(f"wrote documentation to {output}.")


if __name__ == '__main__':
    main()
