#!/usr/bin/env python
#
# Copyright Contributors to the OpenTimelineIO project
#
# licensed under the apache license, version 2.0 (the "apache license")
# with the following modification; you may not use this file except in
# compliance with the apache license and the following modification to it:
# section 6. trademarks. is deleted and replaced with:
#
# 6. trademarks. this license does not grant permission to use the trade
#    names, trademarks, service marks, or product names of the licensor
#    and its affiliates, except as required to comply with section 4(c) of
#    the license and to reproduce the content of the notice file.
#
# you may obtain a copy of the apache license at
#
#     http://www.apache.org/licenses/license-2.0
#
# unless required by applicable law or agreed to in writing, software
# distributed under the apache license with the above modification is
# distributed on an "as is" basis, without warranties or conditions of any
# kind, either express or implied. see the apache license for the specific
# language governing permissions and limitations under the apache license.
#

"""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 os

try:
    # python2
    import StringIO as io
except ImportError:
    # python3
    import io

import opentimelineio as otio


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 and
contrib manifests are listed first, then any user-defined local plugins.

{manifests}

# Core Plugins

Manifest path: `{manifest_path}`

{manifest_contents}

# Contrib Plugins

Plugins in Contrib are supported by the community and provided as-is.

Manifest path: `{contrib_manifest_path}`

{contrib_manifest_contents}

{local_manifest_text}
"""

MANIFEST_CONTENT_TEMPLATE = """

## Adapter Plugins

Adapter plugins convert to and from OpenTimelineIO.

<a href="adapters.html" target="_blank"> Adapters documentation page for more
information</a>

<a href="write-an-adapter.html" target="_blank">Tutorial on how to write an
adapter.</a>

{adapters}

## Media Linkers

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

<a href="write-a-media-linker.html" target="_blank"> Tutorial on how to write a
Media Linker</a>

{media_linkers}

## SchemaDefs

SchemaDef plugins define new external schema.

<a href="write-a-schemadef.html" target="_blank"> Tutorial on how to write a
schemadef</a>

{schemadefs}

## HookScripts

HookScripts are extra plugins that run on _hooks_.

<a href="write-a-hookscript.html" target="_blank">Tutorial on how to write a
hookscript.</a>

{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):
    path = plugin_map['path']
    if sanitized_paths:
        path = os.path.sep.join(path.split(os.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, "- {}: \n```\n".format(feature)) + "\n```"
            )
        else:
            feature_lines.append(
                "- {}:".format(feature)
            )

        for arg in feature_data["args"]:
            feature_lines.append("  - {}".format(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, "- {}: \n```\n".format(sd)) + "\n```"
            )
        else:
            feature_lines.append("- {}:".format(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 = []

        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):
    plugin_info_map = otio.plugins.plugin_info_map()

    # start with the manifest list
    md_out = io.StringIO()

    manifest_path_list = plugin_info_map['manifests']

    if public_only:
        manifest_path_list = manifest_path_list[:2]

    sanitized_paths = manifest_path_list[:]
    if sanitized_paths:
        sanitized_paths = [
            os.path.sep.join(p.split(os.path.sep)[-3:])
            for p in manifest_path_list
        ]

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

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

    contrib_manifest_path = manifest_path_list[1]
    contrib_manifest_path_sanitized = sanitized_paths[1]
    contrib_manifest_text = _manifest_formatted(
        plugin_info_map,
        [contrib_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_paths[2:]
        local_manifest_list = "\n".join(
            "- `{}`".format(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,
            contrib_manifest_path=contrib_manifest_path_sanitized,
            contrib_manifest_contents=contrib_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("wrote documentation to {}.".format(output))


if __name__ == '__main__':
    main()
